1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * 4 * Zimbra Collaboration Suite Web Client 5 * Copyright (C) 2012 VMware, Inc. 6 * 7 * The contents of this file are subject to the Zimbra Public License 8 * Version 1.3 ("License"); you may not use this file except in 9 * compliance with the License. You may obtain a copy of the License at 10 * http://www.zimbra.com/license. 11 * 12 * Software distributed under the License is distributed on an "AS IS" 13 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. 14 * 15 * ***** END LICENSE BLOCK ***** 16 */ 17 // FILE IS GENERATED BY COMBINING THE SOURCES IN THE "classes" DIRECTORY SO DON'T MODIFY THIS FILE DIRECTLY 18 (function(win) { 19 var whiteSpaceRe = /^\s*|\s*$/g, 20 undef, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1'; 21 22 var tinymce = { 23 majorVersion : '3', 24 25 minorVersion : '5.4.1', 26 27 releaseDate : '2012-06-24', 28 29 _init : function() { 30 var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v; 31 32 t.isOpera = win.opera && opera.buildNumber; 33 34 t.isWebKit = /WebKit/.test(ua); 35 36 t.isIE = !t.isWebKit && !t.isOpera && (/MSIE/gi).test(ua) && (/Explorer/gi).test(na.appName); 37 38 t.isIE6 = t.isIE && /MSIE [56]/.test(ua); 39 40 t.isIE7 = t.isIE && /MSIE [7]/.test(ua); 41 42 t.isIE8 = t.isIE && /MSIE [8]/.test(ua); 43 44 t.isIE9 = t.isIE && /MSIE [9]/.test(ua); 45 46 t.isGecko = !t.isWebKit && /Gecko/.test(ua); 47 48 t.isMac = ua.indexOf('Mac') != -1; 49 50 t.isAir = /adobeair/i.test(ua); 51 52 t.isIDevice = /(iPad|iPhone)/.test(ua); 53 54 t.isIOS5 = t.isIDevice && ua.match(/AppleWebKit\/(\d*)/)[1]>=534; 55 56 // TinyMCE .NET webcontrol might be setting the values for TinyMCE 57 if (win.tinyMCEPreInit) { 58 t.suffix = tinyMCEPreInit.suffix; 59 t.baseURL = tinyMCEPreInit.base; 60 t.query = tinyMCEPreInit.query; 61 return; 62 } 63 64 // Get suffix and base 65 t.suffix = ''; 66 67 // If base element found, add that infront of baseURL 68 nl = d.getElementsByTagName('base'); 69 for (i=0; i<nl.length; i++) { 70 v = nl[i].href; 71 if (v) { 72 // Host only value like http://site.com or http://site.com:8008 73 if (/^https?:\/\/[^\/]+$/.test(v)) 74 v += '/'; 75 76 base = v ? v.match(/.*\//)[0] : ''; // Get only directory 77 } 78 } 79 80 function getBase(n) { 81 if (n.src && /tiny_mce(|_gzip|_jquery|_prototype|_full)(_dev|_src)?.js/.test(n.src)) { 82 if (/_(src|dev)\.js/g.test(n.src)) 83 t.suffix = '_src'; 84 85 if ((p = n.src.indexOf('?')) != -1) 86 t.query = n.src.substring(p + 1); 87 88 t.baseURL = n.src.substring(0, n.src.lastIndexOf('/')); 89 90 // If path to script is relative and a base href was found add that one infront 91 // the src property will always be an absolute one on non IE browsers and IE 8 92 // so this logic will basically only be executed on older IE versions 93 if (base && t.baseURL.indexOf('://') == -1 && t.baseURL.indexOf('/') !== 0) 94 t.baseURL = base + t.baseURL; 95 96 return t.baseURL; 97 } 98 99 return null; 100 }; 101 102 // Check document 103 nl = d.getElementsByTagName('script'); 104 for (i=0; i<nl.length; i++) { 105 if (getBase(nl[i])) 106 return; 107 } 108 109 // Check head 110 n = d.getElementsByTagName('head')[0]; 111 if (n) { 112 nl = n.getElementsByTagName('script'); 113 for (i=0; i<nl.length; i++) { 114 if (getBase(nl[i])) 115 return; 116 } 117 } 118 119 return; 120 }, 121 122 is : function(o, t) { 123 if (!t) 124 return o !== undef; 125 126 if (t == 'array' && (o.hasOwnProperty && o instanceof Array)) 127 return true; 128 129 return typeof(o) == t; 130 }, 131 132 makeMap : function(items, delim, map) { 133 var i; 134 135 items = items || []; 136 delim = delim || ','; 137 138 if (typeof(items) == "string") 139 items = items.split(delim); 140 141 map = map || {}; 142 143 i = items.length; 144 while (i--) 145 map[items[i]] = {}; 146 147 return map; 148 }, 149 150 each : function(o, cb, s) { 151 var n, l; 152 153 if (!o) 154 return 0; 155 156 s = s || o; 157 158 if (o.length !== undef) { 159 // Indexed arrays, needed for Safari 160 for (n=0, l = o.length; n < l; n++) { 161 if (cb.call(s, o[n], n, o) === false) 162 return 0; 163 } 164 } else { 165 // Hashtables 166 for (n in o) { 167 if (o.hasOwnProperty(n)) { 168 if (cb.call(s, o[n], n, o) === false) 169 return 0; 170 } 171 } 172 } 173 174 return 1; 175 }, 176 177 178 trim : function(s) { 179 return (s ? '' + s : '').replace(whiteSpaceRe, ''); 180 }, 181 182 create : function(s, p, root) { 183 var t = this, sp, ns, cn, scn, c, de = 0; 184 185 // Parse : <prefix> <class>:<super class> 186 s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s); 187 cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name 188 189 // Create namespace for new class 190 ns = t.createNS(s[3].replace(/\.\w+$/, ''), root); 191 192 // Class already exists 193 if (ns[cn]) 194 return; 195 196 // Make pure static class 197 if (s[2] == 'static') { 198 ns[cn] = p; 199 200 if (this.onCreate) 201 this.onCreate(s[2], s[3], ns[cn]); 202 203 return; 204 } 205 206 // Create default constructor 207 if (!p[cn]) { 208 p[cn] = function() {}; 209 de = 1; 210 } 211 212 // Add constructor and methods 213 ns[cn] = p[cn]; 214 t.extend(ns[cn].prototype, p); 215 216 // Extend 217 if (s[5]) { 218 sp = t.resolve(s[5]).prototype; 219 scn = s[5].match(/\.(\w+)$/i)[1]; // Class name 220 221 // Extend constructor 222 c = ns[cn]; 223 if (de) { 224 // Add passthrough constructor 225 ns[cn] = function() { 226 return sp[scn].apply(this, arguments); 227 }; 228 } else { 229 // Add inherit constructor 230 ns[cn] = function() { 231 this.parent = sp[scn]; 232 return c.apply(this, arguments); 233 }; 234 } 235 ns[cn].prototype[cn] = ns[cn]; 236 237 // Add super methods 238 t.each(sp, function(f, n) { 239 ns[cn].prototype[n] = sp[n]; 240 }); 241 242 // Add overridden methods 243 t.each(p, function(f, n) { 244 // Extend methods if needed 245 if (sp[n]) { 246 ns[cn].prototype[n] = function() { 247 this.parent = sp[n]; 248 return f.apply(this, arguments); 249 }; 250 } else { 251 if (n != cn) 252 ns[cn].prototype[n] = f; 253 } 254 }); 255 } 256 257 // Add static methods 258 t.each(p['static'], function(f, n) { 259 ns[cn][n] = f; 260 }); 261 262 if (this.onCreate) 263 this.onCreate(s[2], s[3], ns[cn].prototype); 264 }, 265 266 walk : function(o, f, n, s) { 267 s = s || this; 268 269 if (o) { 270 if (n) 271 o = o[n]; 272 273 tinymce.each(o, function(o, i) { 274 if (f.call(s, o, i, n) === false) 275 return false; 276 277 tinymce.walk(o, f, n, s); 278 }); 279 } 280 }, 281 282 createNS : function(n, o) { 283 var i, v; 284 285 o = o || win; 286 287 n = n.split('.'); 288 for (i=0; i<n.length; i++) { 289 v = n[i]; 290 291 if (!o[v]) 292 o[v] = {}; 293 294 o = o[v]; 295 } 296 297 return o; 298 }, 299 300 resolve : function(n, o) { 301 var i, l; 302 303 o = o || win; 304 305 n = n.split('.'); 306 for (i = 0, l = n.length; i < l; i++) { 307 o = o[n[i]]; 308 309 if (!o) 310 break; 311 } 312 313 return o; 314 }, 315 316 addUnload : function(f, s) { 317 var t = this, unload; 318 319 unload = function() { 320 var li = t.unloads, o, n; 321 322 if (li) { 323 // Call unload handlers 324 for (n in li) { 325 o = li[n]; 326 327 if (o && o.func) 328 o.func.call(o.scope, 1); // Send in one arg to distinct unload and user destroy 329 } 330 331 // Detach unload function 332 if (win.detachEvent) { 333 win.detachEvent('onbeforeunload', fakeUnload); 334 win.detachEvent('onunload', unload); 335 } else if (win.removeEventListener) 336 win.removeEventListener('unload', unload, false); 337 338 // Destroy references 339 t.unloads = o = li = w = unload = 0; 340 341 // Run garbarge collector on IE 342 if (win.CollectGarbage) 343 CollectGarbage(); 344 } 345 }; 346 347 function fakeUnload() { 348 var d = document; 349 350 function stop() { 351 // Prevent memory leak 352 d.detachEvent('onstop', stop); 353 354 // Call unload handler 355 if (unload) 356 unload(); 357 358 d = 0; 359 }; 360 361 // Is there things still loading, then do some magic 362 if (d.readyState == 'interactive') { 363 // Fire unload when the currently loading page is stopped 364 if (d) 365 d.attachEvent('onstop', stop); 366 367 // Remove onstop listener after a while to prevent the unload function 368 // to execute if the user presses cancel in an onbeforeunload 369 // confirm dialog and then presses the browser stop button 370 win.setTimeout(function() { 371 if (d) 372 d.detachEvent('onstop', stop); 373 }, 0); 374 } 375 }; 376 377 f = {func : f, scope : s || this}; 378 379 if (!t.unloads) { 380 // Attach unload handler 381 if (win.attachEvent) { 382 win.attachEvent('onunload', unload); 383 win.attachEvent('onbeforeunload', fakeUnload); 384 } else if (win.addEventListener) 385 win.addEventListener('unload', unload, false); 386 387 // Setup initial unload handler array 388 t.unloads = [f]; 389 } else 390 t.unloads.push(f); 391 392 return f; 393 }, 394 395 removeUnload : function(f) { 396 var u = this.unloads, r = null; 397 398 tinymce.each(u, function(o, i) { 399 if (o && o.func == f) { 400 u.splice(i, 1); 401 r = f; 402 return false; 403 } 404 }); 405 406 return r; 407 }, 408 409 explode : function(s, d) { 410 if (!s || tinymce.is(s, 'array')) { 411 return s; 412 } 413 414 return tinymce.map(s.split(d || ','), tinymce.trim); 415 }, 416 417 _addVer : function(u) { 418 var v; 419 420 if (!this.query) 421 return u; 422 423 v = (u.indexOf('?') == -1 ? '?' : '&') + this.query; 424 425 if (u.indexOf('#') == -1) 426 return u + v; 427 428 return u.replace('#', v + '#'); 429 }, 430 431 // Fix function for IE 9 where regexps isn't working correctly 432 // Todo: remove me once MS fixes the bug 433 _replace : function(find, replace, str) { 434 // On IE9 we have to fake $x replacement 435 if (isRegExpBroken) { 436 return str.replace(find, function() { 437 var val = replace, args = arguments, i; 438 439 for (i = 0; i < args.length - 2; i++) { 440 if (args[i] === undef) { 441 val = val.replace(new RegExp('\\$' + i, 'g'), ''); 442 } else { 443 val = val.replace(new RegExp('\\$' + i, 'g'), args[i]); 444 } 445 } 446 447 return val; 448 }); 449 } 450 451 return str.replace(find, replace); 452 } 453 454 }; 455 456 // Initialize the API 457 tinymce._init(); 458 459 // Expose tinymce namespace to the global namespace (window) 460 win.tinymce = win.tinyMCE = tinymce; 461 462 // Describe the different namespaces 463 464 })(window); 465 466 467 (function($, tinymce) { 468 var is = tinymce.is, attrRegExp = /^(href|src|style)$/i, undef; 469 470 // jQuery is undefined 471 if (!$ && window.console) { 472 return console.log("Load jQuery first!"); 473 } 474 475 // Stick jQuery into the tinymce namespace 476 tinymce.$ = $; 477 478 // Setup adapter 479 tinymce.adapter = { 480 patchEditor : function(editor) { 481 var fn = $.fn; 482 483 // Adapt the css function to make sure that the data-mce-style 484 // attribute gets updated with the new style information 485 function css(name, value) { 486 var self = this; 487 488 // Remove data-mce-style when set operation occurs 489 if (value) 490 self.removeAttr('data-mce-style'); 491 492 return fn.css.apply(self, arguments); 493 }; 494 495 // Apapt the attr function to make sure that it uses the data-mce- prefixed variants 496 function attr(name, value) { 497 var self = this; 498 499 // Update/retrive data-mce- attribute variants 500 if (attrRegExp.test(name)) { 501 if (value !== undef) { 502 // Use TinyMCE behavior when setting the specifc attributes 503 self.each(function(i, node) { 504 editor.dom.setAttrib(node, name, value); 505 }); 506 507 return self; 508 } else 509 return self.attr('data-mce-' + name); 510 } 511 512 // Default behavior 513 return fn.attr.apply(self, arguments); 514 }; 515 516 // Patch various jQuery functions to handle tinymce specific attribute and content behavior 517 // we don't patch the jQuery.fn directly since it will most likely break compatibility 518 // with other jQuery logic on the page. Only instances created by TinyMCE should be patched. 519 function patch(jq) { 520 // Patch some functions, only patch the object once 521 if (jq.css !== css) { 522 // Patch css/attr to use the data-mce- prefixed attribute variants 523 jq.css = css; 524 jq.attr = attr; 525 526 jq.tinymce = editor; 527 528 // Each pushed jQuery instance needs to be patched 529 // as well for example when traversing the DOM 530 jq.pushStack = function() { 531 return patch(fn.pushStack.apply(this, arguments)); 532 }; 533 } 534 535 return jq; 536 }; 537 538 // Add a $ function on each editor instance this one is scoped for the editor document object 539 // this way you can do chaining like this tinymce.get(0).$('p').append('text').css('color', 'red'); 540 editor.$ = function(selector, scope) { 541 var doc = editor.getDoc(); 542 543 return patch($(selector || doc, doc || scope)); 544 }; 545 } 546 }; 547 548 // Patch in core NS functions 549 tinymce.extend = $.extend; 550 tinymce.extend(tinymce, { 551 map : $.map, 552 grep : function(a, f) {return $.grep(a, f || function(){return 1;});}, 553 inArray : function(a, v) {return $.inArray(v, a || []);} 554 555 /* Didn't iterate stylesheets 556 each : function(o, cb, s) { 557 if (!o) 558 return 0; 559 560 var r = 1; 561 562 $.each(o, function(nr, el){ 563 if (cb.call(s, el, nr, o) === false) { 564 r = 0; 565 return false; 566 } 567 }); 568 569 return r; 570 }*/ 571 }); 572 573 // Patch in functions in various clases 574 // Add a "#ifndefjquery" statement around each core API function you add below 575 var patches = { 576 'tinymce.dom.DOMUtils' : { 577 /* 578 addClass : function(e, c) { 579 if (is(e, 'array') && is(e[0], 'string')) 580 e = e.join(',#'); 581 return (e && $(is(e, 'string') ? '#' + e : e) 582 .addClass(c) 583 .attr('class')) || false; 584 }, 585 586 hasClass : function(n, c) { 587 return $(is(n, 'string') ? '#' + n : n).hasClass(c); 588 }, 589 590 removeClass : function(e, c) { 591 if (!e) 592 return false; 593 594 var r = []; 595 596 $(is(e, 'string') ? '#' + e : e) 597 .removeClass(c) 598 .each(function(){ 599 r.push(this.className); 600 }); 601 602 return r.length == 1 ? r[0] : r; 603 }, 604 */ 605 606 select : function(pattern, scope) { 607 var t = this; 608 609 return $.find(pattern, t.get(scope) || t.get(t.settings.root_element) || t.doc, []); 610 }, 611 612 is : function(n, patt) { 613 return $(this.get(n)).is(patt); 614 } 615 616 /* 617 show : function(e) { 618 if (is(e, 'array') && is(e[0], 'string')) 619 e = e.join(',#'); 620 621 $(is(e, 'string') ? '#' + e : e).css('display', 'block'); 622 }, 623 624 hide : function(e) { 625 if (is(e, 'array') && is(e[0], 'string')) 626 e = e.join(',#'); 627 628 $(is(e, 'string') ? '#' + e : e).css('display', 'none'); 629 }, 630 631 isHidden : function(e) { 632 return $(is(e, 'string') ? '#' + e : e).is(':hidden'); 633 }, 634 635 insertAfter : function(n, e) { 636 return $(is(e, 'string') ? '#' + e : e).after(n); 637 }, 638 639 replace : function(o, n, k) { 640 n = $(is(n, 'string') ? '#' + n : n); 641 642 if (k) 643 n.children().appendTo(o); 644 645 n.replaceWith(o); 646 }, 647 648 setStyle : function(n, na, v) { 649 if (is(n, 'array') && is(n[0], 'string')) 650 n = n.join(',#'); 651 652 $(is(n, 'string') ? '#' + n : n).css(na, v); 653 }, 654 655 getStyle : function(n, na, c) { 656 return $(is(n, 'string') ? '#' + n : n).css(na); 657 }, 658 659 setStyles : function(e, o) { 660 if (is(e, 'array') && is(e[0], 'string')) 661 e = e.join(',#'); 662 $(is(e, 'string') ? '#' + e : e).css(o); 663 }, 664 665 setAttrib : function(e, n, v) { 666 var t = this, s = t.settings; 667 668 if (is(e, 'array') && is(e[0], 'string')) 669 e = e.join(',#'); 670 671 e = $(is(e, 'string') ? '#' + e : e); 672 673 switch (n) { 674 case "style": 675 e.each(function(i, v){ 676 if (s.keep_values) 677 $(v).attr('data-mce-style', v); 678 679 v.style.cssText = v; 680 }); 681 break; 682 683 case "class": 684 e.each(function(){ 685 this.className = v; 686 }); 687 break; 688 689 case "src": 690 case "href": 691 e.each(function(i, v){ 692 if (s.keep_values) { 693 if (s.url_converter) 694 v = s.url_converter.call(s.url_converter_scope || t, v, n, v); 695 696 t.setAttrib(v, 'data-mce-' + n, v); 697 } 698 }); 699 700 break; 701 } 702 703 if (v !== null && v.length !== 0) 704 e.attr(n, '' + v); 705 else 706 e.removeAttr(n); 707 }, 708 709 setAttribs : function(e, o) { 710 var t = this; 711 712 $.each(o, function(n, v){ 713 t.setAttrib(e,n,v); 714 }); 715 } 716 */ 717 } 718 719 /* 720 'tinymce.dom.Event' : { 721 add : function (o, n, f, s) { 722 var lo, cb; 723 724 cb = function(e) { 725 e.target = e.target || this; 726 f.call(s || this, e); 727 }; 728 729 if (is(o, 'array') && is(o[0], 'string')) 730 o = o.join(',#'); 731 o = $(is(o, 'string') ? '#' + o : o); 732 if (n == 'init') { 733 o.ready(cb, s); 734 } else { 735 if (s) { 736 o.bind(n, s, cb); 737 } else { 738 o.bind(n, cb); 739 } 740 } 741 742 lo = this._jqLookup || (this._jqLookup = []); 743 lo.push({func : f, cfunc : cb}); 744 745 return cb; 746 }, 747 748 remove : function(o, n, f) { 749 // Find cfunc 750 $(this._jqLookup).each(function() { 751 if (this.func === f) 752 f = this.cfunc; 753 }); 754 755 if (is(o, 'array') && is(o[0], 'string')) 756 o = o.join(',#'); 757 758 $(is(o, 'string') ? '#' + o : o).unbind(n,f); 759 760 return true; 761 } 762 } 763 */ 764 }; 765 766 // Patch functions after a class is created 767 tinymce.onCreate = function(ty, c, p) { 768 tinymce.extend(p, patches[c]); 769 }; 770 })(window.jQuery, tinymce); 771 772 773 774 tinymce.create('tinymce.util.Dispatcher', { 775 scope : null, 776 listeners : null, 777 inDispatch: false, 778 779 Dispatcher : function(scope) { 780 this.scope = scope || this; 781 this.listeners = []; 782 }, 783 784 add : function(callback, scope) { 785 this.listeners.push({cb : callback, scope : scope || this.scope}); 786 787 return callback; 788 }, 789 790 addToTop : function(callback, scope) { 791 var self = this, listener = {cb : callback, scope : scope || self.scope}; 792 793 // Create new listeners if addToTop is executed in a dispatch loop 794 if (self.inDispatch) { 795 self.listeners = [listener].concat(self.listeners); 796 } else { 797 self.listeners.unshift(listener); 798 } 799 800 return callback; 801 }, 802 803 remove : function(callback) { 804 var listeners = this.listeners, output = null; 805 806 tinymce.each(listeners, function(listener, i) { 807 if (callback == listener.cb) { 808 output = listener; 809 listeners.splice(i, 1); 810 return false; 811 } 812 }); 813 814 return output; 815 }, 816 817 dispatch : function() { 818 var self = this, returnValue, args = arguments, i, listeners = self.listeners, listener; 819 820 self.inDispatch = true; 821 822 // Needs to be a real loop since the listener count might change while looping 823 // And this is also more efficient 824 for (i = 0; i < listeners.length; i++) { 825 listener = listeners[i]; 826 returnValue = listener.cb.apply(listener.scope, args.length > 0 ? args : [listener.scope]); 827 828 if (returnValue === false) 829 break; 830 } 831 832 self.inDispatch = false; 833 834 return returnValue; 835 } 836 837 }); 838 839 (function() { 840 var each = tinymce.each; 841 842 tinymce.create('tinymce.util.URI', { 843 URI : function(u, s) { 844 var t = this, o, a, b, base_url; 845 846 // Trim whitespace 847 u = tinymce.trim(u); 848 849 // Default settings 850 s = t.settings = s || {}; 851 852 // Strange app protocol that isn't http/https or local anchor 853 // For example: mailto,skype,tel etc. 854 if (/^([\w\-]+):([^\/]{2})/i.test(u) || /^\s*#/.test(u)) { 855 t.source = u; 856 return; 857 } 858 859 // Absolute path with no host, fake host and protocol 860 if (u.indexOf('/') === 0 && u.indexOf('//') !== 0) 861 u = (s.base_uri ? s.base_uri.protocol || 'http' : 'http') + '://mce_host' + u; 862 863 // Relative path http:// or protocol relative //path 864 if (!/^[\w\-]*:?\/\//.test(u)) { 865 base_url = s.base_uri ? s.base_uri.path : new tinymce.util.URI(location.href).directory; 866 u = ((s.base_uri && s.base_uri.protocol) || 'http') + '://mce_host' + t.toAbsPath(base_url, u); 867 } 868 869 // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) 870 u = u.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something 871 u = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(u); 872 each(["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], function(v, i) { 873 var s = u[i]; 874 875 // Zope 3 workaround, they use @@something 876 if (s) 877 s = s.replace(/\(mce_at\)/g, '@@'); 878 879 t[v] = s; 880 }); 881 882 b = s.base_uri; 883 if (b) { 884 if (!t.protocol) 885 t.protocol = b.protocol; 886 887 if (!t.userInfo) 888 t.userInfo = b.userInfo; 889 890 if (!t.port && t.host === 'mce_host') 891 t.port = b.port; 892 893 if (!t.host || t.host === 'mce_host') 894 t.host = b.host; 895 896 t.source = ''; 897 } 898 899 //t.path = t.path || '/'; 900 }, 901 902 setPath : function(p) { 903 var t = this; 904 905 p = /^(.*?)\/?(\w+)?$/.exec(p); 906 907 // Update path parts 908 t.path = p[0]; 909 t.directory = p[1]; 910 t.file = p[2]; 911 912 // Rebuild source 913 t.source = ''; 914 t.getURI(); 915 }, 916 917 toRelative : function(u) { 918 var t = this, o; 919 920 if (u === "./") 921 return u; 922 923 u = new tinymce.util.URI(u, {base_uri : t}); 924 925 // Not on same domain/port or protocol 926 if ((u.host != 'mce_host' && t.host != u.host && u.host) || t.port != u.port || t.protocol != u.protocol) 927 return u.getURI(); 928 929 var tu = t.getURI(), uu = u.getURI(); 930 931 // Allow usage of the base_uri when relative_urls = true 932 if(tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) 933 return tu; 934 935 o = t.toRelPath(t.path, u.path); 936 937 // Add query 938 if (u.query) 939 o += '?' + u.query; 940 941 // Add anchor 942 if (u.anchor) 943 o += '#' + u.anchor; 944 945 return o; 946 }, 947 948 toAbsolute : function(u, nh) { 949 u = new tinymce.util.URI(u, {base_uri : this}); 950 951 return u.getURI(this.host == u.host && this.protocol == u.protocol ? nh : 0); 952 }, 953 954 toRelPath : function(base, path) { 955 var items, bp = 0, out = '', i, l; 956 957 // Split the paths 958 base = base.substring(0, base.lastIndexOf('/')); 959 base = base.split('/'); 960 items = path.split('/'); 961 962 if (base.length >= items.length) { 963 for (i = 0, l = base.length; i < l; i++) { 964 if (i >= items.length || base[i] != items[i]) { 965 bp = i + 1; 966 break; 967 } 968 } 969 } 970 971 if (base.length < items.length) { 972 for (i = 0, l = items.length; i < l; i++) { 973 if (i >= base.length || base[i] != items[i]) { 974 bp = i + 1; 975 break; 976 } 977 } 978 } 979 980 if (bp === 1) 981 return path; 982 983 for (i = 0, l = base.length - (bp - 1); i < l; i++) 984 out += "../"; 985 986 for (i = bp - 1, l = items.length; i < l; i++) { 987 if (i != bp - 1) 988 out += "/" + items[i]; 989 else 990 out += items[i]; 991 } 992 993 return out; 994 }, 995 996 toAbsPath : function(base, path) { 997 var i, nb = 0, o = [], tr, outPath; 998 999 // Split paths 1000 tr = /\/$/.test(path) ? '/' : ''; 1001 base = base.split('/'); 1002 path = path.split('/'); 1003 1004 // Remove empty chunks 1005 each(base, function(k) { 1006 if (k) 1007 o.push(k); 1008 }); 1009 1010 base = o; 1011 1012 // Merge relURLParts chunks 1013 for (i = path.length - 1, o = []; i >= 0; i--) { 1014 // Ignore empty or . 1015 if (path[i].length === 0 || path[i] === ".") 1016 continue; 1017 1018 // Is parent 1019 if (path[i] === '..') { 1020 nb++; 1021 continue; 1022 } 1023 1024 // Move up 1025 if (nb > 0) { 1026 nb--; 1027 continue; 1028 } 1029 1030 o.push(path[i]); 1031 } 1032 1033 i = base.length - nb; 1034 1035 // If /a/b/c or / 1036 if (i <= 0) 1037 outPath = o.reverse().join('/'); 1038 else 1039 outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/'); 1040 1041 // Add front / if it's needed 1042 if (outPath.indexOf('/') !== 0) 1043 outPath = '/' + outPath; 1044 1045 // Add traling / if it's needed 1046 if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) 1047 outPath += tr; 1048 1049 return outPath; 1050 }, 1051 1052 getURI : function(nh) { 1053 var s, t = this; 1054 1055 // Rebuild source 1056 if (!t.source || nh) { 1057 s = ''; 1058 1059 if (!nh) { 1060 if (t.protocol) 1061 s += t.protocol + '://'; 1062 1063 if (t.userInfo) 1064 s += t.userInfo + '@'; 1065 1066 if (t.host) 1067 s += t.host; 1068 1069 if (t.port) 1070 s += ':' + t.port; 1071 } 1072 1073 if (t.path) 1074 s += t.path; 1075 1076 if (t.query) 1077 s += '?' + t.query; 1078 1079 if (t.anchor) 1080 s += '#' + t.anchor; 1081 1082 t.source = s; 1083 } 1084 1085 return t.source; 1086 } 1087 }); 1088 })(); 1089 1090 (function() { 1091 var each = tinymce.each; 1092 1093 tinymce.create('static tinymce.util.Cookie', { 1094 getHash : function(n) { 1095 var v = this.get(n), h; 1096 1097 if (v) { 1098 each(v.split('&'), function(v) { 1099 v = v.split('='); 1100 h = h || {}; 1101 h[unescape(v[0])] = unescape(v[1]); 1102 }); 1103 } 1104 1105 return h; 1106 }, 1107 1108 setHash : function(n, v, e, p, d, s) { 1109 var o = ''; 1110 1111 each(v, function(v, k) { 1112 o += (!o ? '' : '&') + escape(k) + '=' + escape(v); 1113 }); 1114 1115 this.set(n, o, e, p, d, s); 1116 }, 1117 1118 get : function(n) { 1119 var c = document.cookie, e, p = n + "=", b; 1120 1121 // Strict mode 1122 if (!c) 1123 return; 1124 1125 b = c.indexOf("; " + p); 1126 1127 if (b == -1) { 1128 b = c.indexOf(p); 1129 1130 if (b !== 0) 1131 return null; 1132 } else 1133 b += 2; 1134 1135 e = c.indexOf(";", b); 1136 1137 if (e == -1) 1138 e = c.length; 1139 1140 return unescape(c.substring(b + p.length, e)); 1141 }, 1142 1143 set : function(n, v, e, p, d, s) { 1144 document.cookie = n + "=" + escape(v) + 1145 ((e) ? "; expires=" + e.toGMTString() : "") + 1146 ((p) ? "; path=" + escape(p) : "") + 1147 ((d) ? "; domain=" + d : "") + 1148 ((s) ? "; secure" : ""); 1149 }, 1150 1151 remove : function(name, path, domain) { 1152 var date = new Date(); 1153 1154 date.setTime(date.getTime() - 1000); 1155 1156 this.set(name, '', date, path, domain); 1157 } 1158 }); 1159 })(); 1160 1161 (function() { 1162 function serialize(o, quote) { 1163 var i, v, t, name; 1164 1165 quote = quote || '"'; 1166 1167 if (o == null) 1168 return 'null'; 1169 1170 t = typeof o; 1171 1172 if (t == 'string') { 1173 v = '\bb\tt\nn\ff\rr\""\'\'\\\\'; 1174 1175 return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) { 1176 // Make sure single quotes never get encoded inside double quotes for JSON compatibility 1177 if (quote === '"' && a === "'") 1178 return a; 1179 1180 i = v.indexOf(b); 1181 1182 if (i + 1) 1183 return '\\' + v.charAt(i + 1); 1184 1185 a = b.charCodeAt().toString(16); 1186 1187 return '\\u' + '0000'.substring(a.length) + a; 1188 }) + quote; 1189 } 1190 1191 if (t == 'object') { 1192 if (o.hasOwnProperty && o instanceof Array) { 1193 for (i=0, v = '['; i<o.length; i++) 1194 v += (i > 0 ? ',' : '') + serialize(o[i], quote); 1195 1196 return v + ']'; 1197 } 1198 1199 v = '{'; 1200 1201 for (name in o) { 1202 if (o.hasOwnProperty(name)) { 1203 v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name + quote +':' + serialize(o[name], quote) : ''; 1204 } 1205 } 1206 1207 return v + '}'; 1208 } 1209 1210 return '' + o; 1211 }; 1212 1213 tinymce.util.JSON = { 1214 serialize: serialize, 1215 1216 parse: function(s) { 1217 try { 1218 return eval('(' + s + ')'); 1219 } catch (ex) { 1220 // Ignore 1221 } 1222 } 1223 1224 }; 1225 })(); 1226 1227 tinymce.create('static tinymce.util.XHR', { 1228 send : function(o) { 1229 var x, t, w = window, c = 0; 1230 1231 function ready() { 1232 if (!o.async || x.readyState == 4 || c++ > 10000) { 1233 if (o.success && c < 10000 && x.status == 200) 1234 o.success.call(o.success_scope, '' + x.responseText, x, o); 1235 else if (o.error) 1236 o.error.call(o.error_scope, c > 10000 ? 'TIMED_OUT' : 'GENERAL', x, o); 1237 1238 x = null; 1239 } else 1240 w.setTimeout(ready, 10); 1241 }; 1242 1243 // Default settings 1244 o.scope = o.scope || this; 1245 o.success_scope = o.success_scope || o.scope; 1246 o.error_scope = o.error_scope || o.scope; 1247 o.async = o.async === false ? false : true; 1248 o.data = o.data || ''; 1249 1250 function get(s) { 1251 x = 0; 1252 1253 try { 1254 x = new ActiveXObject(s); 1255 } catch (ex) { 1256 } 1257 1258 return x; 1259 }; 1260 1261 x = w.XMLHttpRequest ? new XMLHttpRequest() : get('Microsoft.XMLHTTP') || get('Msxml2.XMLHTTP'); 1262 1263 if (x) { 1264 if (x.overrideMimeType) 1265 x.overrideMimeType(o.content_type); 1266 1267 x.open(o.type || (o.data ? 'POST' : 'GET'), o.url, o.async); 1268 1269 if (o.content_type) 1270 x.setRequestHeader('Content-Type', o.content_type); 1271 1272 x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 1273 1274 x.send(o.data); 1275 1276 // Syncronous request 1277 if (!o.async) 1278 return ready(); 1279 1280 // Wait for response, onReadyStateChange can not be used since it leaks memory in IE 1281 t = w.setTimeout(ready, 10); 1282 } 1283 } 1284 }); 1285 1286 (function() { 1287 var extend = tinymce.extend, JSON = tinymce.util.JSON, XHR = tinymce.util.XHR; 1288 1289 tinymce.create('tinymce.util.JSONRequest', { 1290 JSONRequest : function(s) { 1291 this.settings = extend({ 1292 }, s); 1293 this.count = 0; 1294 }, 1295 1296 send : function(o) { 1297 var ecb = o.error, scb = o.success; 1298 1299 o = extend(this.settings, o); 1300 1301 o.success = function(c, x) { 1302 c = JSON.parse(c); 1303 1304 if (typeof(c) == 'undefined') { 1305 c = { 1306 error : 'JSON Parse error.' 1307 }; 1308 } 1309 1310 if (c.error) 1311 ecb.call(o.error_scope || o.scope, c.error, x); 1312 else 1313 scb.call(o.success_scope || o.scope, c.result); 1314 }; 1315 1316 o.error = function(ty, x) { 1317 if (ecb) 1318 ecb.call(o.error_scope || o.scope, ty, x); 1319 }; 1320 1321 o.data = JSON.serialize({ 1322 id : o.id || 'c' + (this.count++), 1323 method : o.method, 1324 params : o.params 1325 }); 1326 1327 // JSON content type for Ruby on rails. Bug: #1883287 1328 o.content_type = 'application/json'; 1329 1330 XHR.send(o); 1331 }, 1332 1333 'static' : { 1334 sendRPC : function(o) { 1335 return new tinymce.util.JSONRequest().send(o); 1336 } 1337 } 1338 }); 1339 }()); 1340 (function(tinymce){ 1341 tinymce.VK = { 1342 BACKSPACE: 8, 1343 DELETE: 46, 1344 DOWN: 40, 1345 ENTER: 13, 1346 LEFT: 37, 1347 RIGHT: 39, 1348 SPACEBAR: 32, 1349 TAB: 9, 1350 UP: 38, 1351 1352 modifierPressed: function (e) { 1353 return e.shiftKey || e.ctrlKey || e.altKey; 1354 }, 1355 1356 metaKeyPressed: function(e) { 1357 return tinymce.isMac ? e.metaKey : e.ctrlKey; 1358 } 1359 }; 1360 })(tinymce); 1361 1362 tinymce.util.Quirks = function(editor) { 1363 var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, settings = editor.settings; 1364 1365 function setEditorCommandState(cmd, state) { 1366 try { 1367 editor.getDoc().execCommand(cmd, false, state); 1368 } catch (ex) { 1369 // Ignore 1370 } 1371 } 1372 1373 function getDocumentMode() { 1374 var documentMode = editor.getDoc().documentMode; 1375 1376 return documentMode ? documentMode : 6; 1377 }; 1378 1379 function cleanupStylesWhenDeleting() { 1380 function removeMergedFormatSpans(isDelete) { 1381 var rng, blockElm, node, clonedSpan; 1382 1383 rng = selection.getRng(); 1384 1385 // Find root block 1386 blockElm = dom.getParent(rng.startContainer, dom.isBlock); 1387 1388 // On delete clone the root span of the next block element 1389 if (isDelete) 1390 blockElm = dom.getNext(blockElm, dom.isBlock); 1391 1392 // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace 1393 if (blockElm) { 1394 node = blockElm.firstChild; 1395 1396 // Ignore empty text nodes 1397 while (node && node.nodeType == 3 && node.nodeValue.length === 0) 1398 node = node.nextSibling; 1399 1400 if (node && node.nodeName === 'SPAN') { 1401 clonedSpan = node.cloneNode(false); 1402 } 1403 } 1404 1405 // Do the backspace/delete action 1406 editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); 1407 1408 // Find all odd apple-style-spans 1409 blockElm = dom.getParent(rng.startContainer, dom.isBlock); 1410 tinymce.each(dom.select('span.Apple-style-span,font.Apple-style-span', blockElm), function(span) { 1411 var bm = selection.getBookmark(); 1412 1413 if (clonedSpan) { 1414 dom.replace(clonedSpan.cloneNode(false), span, true); 1415 } else { 1416 dom.remove(span, true); 1417 } 1418 1419 // Restore the selection 1420 selection.moveToBookmark(bm); 1421 }); 1422 }; 1423 1424 editor.onKeyDown.add(function(editor, e) { 1425 var isDelete; 1426 1427 isDelete = e.keyCode == DELETE; 1428 if (!e.isDefaultPrevented() && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { 1429 e.preventDefault(); 1430 removeMergedFormatSpans(isDelete); 1431 } 1432 }); 1433 1434 editor.addCommand('Delete', function() {removeMergedFormatSpans();}); 1435 }; 1436 1437 function emptyEditorWhenDeleting() { 1438 function serializeRng(rng) { 1439 var body = dom.create("body"); 1440 var contents = rng.cloneContents(); 1441 body.appendChild(contents); 1442 return selection.serializer.serialize(body, {format: 'html'}); 1443 } 1444 1445 function allContentsSelected(rng) { 1446 var selection = serializeRng(rng); 1447 1448 var allRng = dom.createRng(); 1449 allRng.selectNode(editor.getBody()); 1450 1451 var allSelection = serializeRng(allRng);//console.log(selection, "----", allSelection); 1452 return selection === allSelection; 1453 } 1454 1455 editor.onKeyDown.add(function(editor, e) { 1456 var keyCode = e.keyCode, isCollapsed; 1457 1458 // Empty the editor if it's needed for example backspace at <p><b>|</b></p> 1459 if (!e.isDefaultPrevented() && (keyCode == DELETE || keyCode == BACKSPACE)) { 1460 isCollapsed = editor.selection.isCollapsed(); 1461 1462 // Selection is collapsed but the editor isn't empty 1463 if (isCollapsed && !dom.isEmpty(editor.getBody())) { 1464 return; 1465 } 1466 1467 // IE deletes all contents correctly when everything is selected 1468 if (tinymce.isIE && !isCollapsed) { 1469 return; 1470 } 1471 1472 // Selection isn't collapsed but not all the contents is selected 1473 if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { 1474 return; 1475 } 1476 1477 // Manually empty the editor 1478 editor.setContent(''); 1479 editor.selection.setCursorLocation(editor.getBody(), 0); 1480 editor.nodeChanged(); 1481 } 1482 }); 1483 }; 1484 1485 function selectAll() { 1486 editor.onKeyDown.add(function(editor, e) { 1487 if (e.keyCode == 65 && VK.metaKeyPressed(e)) { 1488 e.preventDefault(); 1489 editor.execCommand('SelectAll'); 1490 } 1491 }); 1492 }; 1493 1494 function inputMethodFocus() { 1495 if (!editor.settings.content_editable) { 1496 // Case 1 IME doesn't initialize if you focus the document 1497 dom.bind(editor.getDoc(), 'focusin', function(e) { 1498 selection.setRng(selection.getRng()); 1499 }); 1500 1501 // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event 1502 dom.bind(editor.getDoc(), 'mousedown', function(e) { 1503 if (e.target == editor.getDoc().documentElement) { 1504 editor.getWin().focus(); 1505 selection.setRng(selection.getRng()); 1506 } 1507 }); 1508 } 1509 }; 1510 1511 function removeHrOnBackspace() { 1512 editor.onKeyDown.add(function(editor, e) { 1513 if (!e.isDefaultPrevented() && e.keyCode === BACKSPACE) { 1514 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 1515 var node = selection.getNode(); 1516 var previousSibling = node.previousSibling; 1517 1518 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { 1519 dom.remove(previousSibling); 1520 tinymce.dom.Event.cancel(e); 1521 } 1522 } 1523 } 1524 }) 1525 } 1526 1527 function focusBody() { 1528 // Fix for a focus bug in FF 3.x where the body element 1529 // wouldn't get proper focus if the user clicked on the HTML element 1530 if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 1531 editor.onMouseDown.add(function(editor, e) { 1532 if (e.target.nodeName === "HTML") { 1533 var body = editor.getBody(); 1534 1535 // Blur the body it's focused but not correctly focused 1536 body.blur(); 1537 1538 // Refocus the body after a little while 1539 setTimeout(function() { 1540 body.focus(); 1541 }, 0); 1542 } 1543 }); 1544 } 1545 }; 1546 1547 function selectControlElements() { 1548 editor.onClick.add(function(editor, e) { 1549 e = e.target; 1550 1551 // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 1552 // WebKit can't even do simple things like selecting an image 1553 // Needs tobe the setBaseAndExtend or it will fail to select floated images 1554 if (/^(IMG|HR)$/.test(e.nodeName)) { 1555 selection.getSel().setBaseAndExtent(e, 0, e, 1); 1556 } 1557 1558 if (e.nodeName == 'A' && dom.hasClass(e, 'mceItemAnchor')) { 1559 selection.select(e); 1560 } 1561 1562 editor.nodeChanged(); 1563 }); 1564 }; 1565 1566 function removeStylesWhenDeletingAccrossBlockElements() { 1567 function getAttributeApplyFunction() { 1568 var template = dom.getAttribs(selection.getStart().cloneNode(false)); 1569 1570 return function() { 1571 var target = selection.getStart(); 1572 1573 if (target !== editor.getBody()) { 1574 dom.setAttrib(target, "style", null); 1575 1576 tinymce.each(template, function(attr) { 1577 target.setAttributeNode(attr.cloneNode(true)); 1578 }); 1579 } 1580 }; 1581 } 1582 1583 function isSelectionAcrossElements() { 1584 return !selection.isCollapsed() && selection.getStart() != selection.getEnd(); 1585 } 1586 1587 function blockEvent(editor, e) { 1588 e.preventDefault(); 1589 return false; 1590 } 1591 1592 editor.onKeyPress.add(function(editor, e) { 1593 var applyAttributes; 1594 1595 if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { 1596 applyAttributes = getAttributeApplyFunction(); 1597 editor.getDoc().execCommand('delete', false, null); 1598 applyAttributes(); 1599 e.preventDefault(); 1600 return false; 1601 } 1602 }); 1603 1604 dom.bind(editor.getDoc(), 'cut', function(e) { 1605 var applyAttributes; 1606 1607 if (isSelectionAcrossElements()) { 1608 applyAttributes = getAttributeApplyFunction(); 1609 editor.onKeyUp.addToTop(blockEvent); 1610 1611 setTimeout(function() { 1612 applyAttributes(); 1613 editor.onKeyUp.remove(blockEvent); 1614 }, 0); 1615 } 1616 }); 1617 } 1618 1619 function selectionChangeNodeChanged() { 1620 var lastRng, selectionTimer; 1621 1622 dom.bind(editor.getDoc(), 'selectionchange', function() { 1623 if (selectionTimer) { 1624 clearTimeout(selectionTimer); 1625 selectionTimer = 0; 1626 } 1627 1628 selectionTimer = window.setTimeout(function() { 1629 var rng = selection.getRng(); 1630 1631 // Compare the ranges to see if it was a real change or not 1632 if (!lastRng || !tinymce.dom.RangeUtils.compareRanges(rng, lastRng)) { 1633 editor.nodeChanged(); 1634 lastRng = rng; 1635 } 1636 }, 50); 1637 }); 1638 } 1639 1640 function ensureBodyHasRoleApplication() { 1641 document.body.setAttribute("role", "application"); 1642 } 1643 1644 function disableBackspaceIntoATable() { 1645 editor.onKeyDown.add(function(editor, e) { 1646 if (!e.isDefaultPrevented() && e.keyCode === BACKSPACE) { 1647 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 1648 var previousSibling = selection.getNode().previousSibling; 1649 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") { 1650 return tinymce.dom.Event.cancel(e); 1651 } 1652 } 1653 } 1654 }) 1655 } 1656 1657 function addNewLinesBeforeBrInPre() { 1658 // IE8+ rendering mode does the right thing with BR in PRE 1659 if (getDocumentMode() > 7) { 1660 return; 1661 } 1662 1663 // Enable display: none in area and add a specific class that hides all BR elements in PRE to 1664 // avoid the caret from getting stuck at the BR elements while pressing the right arrow key 1665 setEditorCommandState('RespectVisibilityInDesign', true); 1666 editor.contentStyles.push('.mceHideBrInPre pre br {display: none}'); 1667 dom.addClass(editor.getBody(), 'mceHideBrInPre'); 1668 1669 // Adds a \n before all BR elements in PRE to get them visual 1670 editor.parser.addNodeFilter('pre', function(nodes, name) { 1671 var i = nodes.length, brNodes, j, brElm, sibling; 1672 1673 while (i--) { 1674 brNodes = nodes[i].getAll('br'); 1675 j = brNodes.length; 1676 while (j--) { 1677 brElm = brNodes[j]; 1678 1679 // Add \n before BR in PRE elements on older IE:s so the new lines get rendered 1680 sibling = brElm.prev; 1681 if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') { 1682 sibling.value += '\n'; 1683 } else { 1684 brElm.parent.insert(new tinymce.html.Node('#text', 3), brElm, true).value = '\n'; 1685 } 1686 } 1687 } 1688 }); 1689 1690 // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible 1691 editor.serializer.addNodeFilter('pre', function(nodes, name) { 1692 var i = nodes.length, brNodes, j, brElm, sibling; 1693 1694 while (i--) { 1695 brNodes = nodes[i].getAll('br'); 1696 j = brNodes.length; 1697 while (j--) { 1698 brElm = brNodes[j]; 1699 sibling = brElm.prev; 1700 if (sibling && sibling.type == 3) { 1701 sibling.value = sibling.value.replace(/\r?\n$/, ''); 1702 } 1703 } 1704 } 1705 }); 1706 } 1707 1708 function removePreSerializedStylesWhenSelectingControls() { 1709 dom.bind(editor.getBody(), 'mouseup', function(e) { 1710 var value, node = selection.getNode(); 1711 1712 // Moved styles to attributes on IMG eements 1713 if (node.nodeName == 'IMG') { 1714 // Convert style width to width attribute 1715 if (value = dom.getStyle(node, 'width')) { 1716 dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, '')); 1717 dom.setStyle(node, 'width', ''); 1718 } 1719 1720 // Convert style height to height attribute 1721 if (value = dom.getStyle(node, 'height')) { 1722 dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, '')); 1723 dom.setStyle(node, 'height', ''); 1724 } 1725 } 1726 }); 1727 } 1728 1729 function keepInlineElementOnDeleteBackspace() { 1730 editor.onKeyDown.add(function(editor, e) { 1731 var isDelete, rng, container, offset, brElm, sibling, collapsed; 1732 1733 isDelete = e.keyCode == DELETE; 1734 if (!e.isDefaultPrevented() && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { 1735 rng = selection.getRng(); 1736 container = rng.startContainer; 1737 offset = rng.startOffset; 1738 collapsed = rng.collapsed; 1739 1740 // Override delete if the start container is a text node and is at the beginning of text or 1741 // just before/after the last character to be deleted in collapsed mode 1742 if (container.nodeType == 3 && container.nodeValue.length > 0 && ((offset === 0 && !collapsed) || (collapsed && offset === (isDelete ? 0 : 1)))) { 1743 nonEmptyElements = editor.schema.getNonEmptyElements(); 1744 1745 // Prevent default logic since it's broken 1746 e.preventDefault(); 1747 1748 // Insert a BR before the text node this will prevent the containing element from being deleted/converted 1749 brElm = dom.create('br', {id: '__tmp'}); 1750 container.parentNode.insertBefore(brElm, container); 1751 1752 // Do the browser delete 1753 editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); 1754 1755 // Check if the previous sibling is empty after deleting for example: <p><b></b>|</p> 1756 container = selection.getRng().startContainer; 1757 sibling = container.previousSibling; 1758 if (sibling && sibling.nodeType == 1 && !dom.isBlock(sibling) && dom.isEmpty(sibling) && !nonEmptyElements[sibling.nodeName.toLowerCase()]) { 1759 dom.remove(sibling); 1760 } 1761 1762 // Remove the temp element we inserted 1763 dom.remove('__tmp'); 1764 } 1765 } 1766 }); 1767 } 1768 1769 function removeBlockQuoteOnBackSpace() { 1770 // Add block quote deletion handler 1771 editor.onKeyDown.add(function(editor, e) { 1772 var rng, container, offset, root, parent; 1773 1774 if (e.isDefaultPrevented() || e.keyCode != VK.BACKSPACE) { 1775 return; 1776 } 1777 1778 rng = selection.getRng(); 1779 container = rng.startContainer; 1780 offset = rng.startOffset; 1781 root = dom.getRoot(); 1782 parent = container; 1783 1784 if (!rng.collapsed || offset !== 0) { 1785 return; 1786 } 1787 1788 while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) { 1789 parent = parent.parentNode; 1790 } 1791 1792 // Is the cursor at the beginning of a blockquote? 1793 if (parent.tagName === 'BLOCKQUOTE') { 1794 // Remove the blockquote 1795 editor.formatter.toggle('blockquote', null, parent); 1796 1797 // Move the caret to the beginning of container 1798 rng.setStart(container, 0); 1799 rng.setEnd(container, 0); 1800 selection.setRng(rng); 1801 selection.collapse(false); 1802 } 1803 }); 1804 }; 1805 1806 function setGeckoEditingOptions() { 1807 function setOpts() { 1808 editor._refreshContentEditable(); 1809 1810 setEditorCommandState("StyleWithCSS", false); 1811 setEditorCommandState("enableInlineTableEditing", false); 1812 1813 if (!settings.object_resizing) { 1814 setEditorCommandState("enableObjectResizing", false); 1815 } 1816 }; 1817 1818 if (!settings.readonly) { 1819 editor.onBeforeExecCommand.add(setOpts); 1820 editor.onMouseDown.add(setOpts); 1821 } 1822 }; 1823 1824 function addBrAfterLastLinks() { 1825 function fixLinks(editor, o) { 1826 tinymce.each(dom.select('a'), function(node) { 1827 var parentNode = node.parentNode, root = dom.getRoot(); 1828 1829 if (parentNode.lastChild === node) { 1830 while (parentNode && !dom.isBlock(parentNode)) { 1831 if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) { 1832 return; 1833 } 1834 1835 parentNode = parentNode.parentNode; 1836 } 1837 1838 dom.add(parentNode, 'br', {'data-mce-bogus' : 1}); 1839 } 1840 }); 1841 }; 1842 1843 editor.onExecCommand.add(function(editor, cmd) { 1844 if (cmd === 'CreateLink') { 1845 fixLinks(editor); 1846 } 1847 }); 1848 1849 editor.onSetContent.add(selection.onSetContent.add(fixLinks)); 1850 }; 1851 1852 function setDefaultBlockType() { 1853 if (settings.forced_root_block) { 1854 editor.onInit.add(function() { 1855 setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block); 1856 }); 1857 } 1858 } 1859 1860 function removeGhostSelection() { 1861 function repaint(sender, args) { 1862 if (!sender || !args.initial) { 1863 editor.execCommand('mceRepaint'); 1864 } 1865 }; 1866 1867 editor.onUndo.add(repaint); 1868 editor.onRedo.add(repaint); 1869 editor.onSetContent.add(repaint); 1870 }; 1871 1872 function deleteControlItemOnBackSpace() { 1873 editor.onKeyDown.add(function(editor, e) { 1874 var rng; 1875 1876 if (!e.isDefaultPrevented() && e.keyCode == BACKSPACE) { 1877 rng = editor.getDoc().selection.createRange(); 1878 if (rng && rng.item) { 1879 e.preventDefault(); 1880 editor.undoManager.beforeChange(); 1881 dom.remove(rng.item(0)); 1882 editor.undoManager.add(); 1883 } 1884 } 1885 }); 1886 }; 1887 1888 function renderEmptyBlocksFix() { 1889 var emptyBlocksCSS; 1890 1891 // IE10+ 1892 if (getDocumentMode() >= 10) { 1893 emptyBlocksCSS = ''; 1894 tinymce.each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) { 1895 emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty'; 1896 }); 1897 1898 editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}'); 1899 } 1900 }; 1901 1902 function fakeImageResize() { 1903 var mouseDownImg, startX, startY, startW, startH; 1904 1905 if (!settings.object_resizing || settings.webkit_fake_resize === false) { 1906 return; 1907 } 1908 1909 editor.contentStyles.push('.mceResizeImages img {cursor: se-resize !important}'); 1910 1911 function resizeImage(e) { 1912 var deltaX, deltaY, ratio, width, height; 1913 1914 if (mouseDownImg) { 1915 deltaX = e.screenX - startX; 1916 deltaY = e.screenY - startY; 1917 ratio = Math.max((startW + deltaX) / startW, (startH + deltaY) / startH); 1918 1919 // Only update styles if the user draged one pixel or more 1920 if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) { 1921 // Constrain proportions 1922 width = Math.round(startW * ratio); 1923 height = Math.round(startH * ratio); 1924 1925 // Resize by using style or attribute 1926 if (mouseDownImg.style.width) { 1927 dom.setStyle(mouseDownImg, 'width', width); 1928 } else { 1929 dom.setAttrib(mouseDownImg, 'width', width); 1930 } 1931 1932 // Resize by using style or attribute 1933 if (mouseDownImg.style.height) { 1934 dom.setStyle(mouseDownImg, 'height', height); 1935 } else { 1936 dom.setAttrib(mouseDownImg, 'height', height); 1937 } 1938 1939 if (!dom.hasClass(editor.getBody(), 'mceResizeImages')) { 1940 dom.addClass(editor.getBody(), 'mceResizeImages'); 1941 } 1942 } 1943 } 1944 }; 1945 1946 editor.onMouseDown.add(function(editor, e) { 1947 var target = e.target; 1948 1949 if (target.nodeName == "IMG") { 1950 mouseDownImg = target; 1951 startX = e.screenX; 1952 startY = e.screenY; 1953 startW = mouseDownImg.clientWidth; 1954 startH = mouseDownImg.clientHeight; 1955 dom.bind(editor.getDoc(), 'mousemove', resizeImage); 1956 e.preventDefault(); 1957 } 1958 }); 1959 1960 // Unbind events on node change and restore resize cursor 1961 editor.onNodeChange.add(function() { 1962 if (mouseDownImg) { 1963 mouseDownImg = null; 1964 dom.unbind(editor.getDoc(), 'mousemove', resizeImage); 1965 } 1966 1967 if (selection.getNode().nodeName == "IMG") { 1968 dom.addClass(editor.getBody(), 'mceResizeImages'); 1969 } else { 1970 dom.removeClass(editor.getBody(), 'mceResizeImages'); 1971 } 1972 }); 1973 }; 1974 1975 // All browsers 1976 disableBackspaceIntoATable(); 1977 removeBlockQuoteOnBackSpace(); 1978 emptyEditorWhenDeleting(); 1979 1980 // WebKit 1981 if (tinymce.isWebKit) { 1982 keepInlineElementOnDeleteBackspace(); 1983 cleanupStylesWhenDeleting(); 1984 inputMethodFocus(); 1985 selectControlElements(); 1986 setDefaultBlockType(); 1987 1988 // iOS 1989 if (tinymce.isIDevice) { 1990 selectionChangeNodeChanged(); 1991 } else { 1992 fakeImageResize(); 1993 selectAll(); 1994 } 1995 } 1996 1997 // IE 1998 if (tinymce.isIE) { 1999 removeHrOnBackspace(); 2000 ensureBodyHasRoleApplication(); 2001 addNewLinesBeforeBrInPre(); 2002 removePreSerializedStylesWhenSelectingControls(); 2003 deleteControlItemOnBackSpace(); 2004 renderEmptyBlocksFix(); 2005 } 2006 2007 // Gecko 2008 if (tinymce.isGecko) { 2009 removeHrOnBackspace(); 2010 focusBody(); 2011 removeStylesWhenDeletingAccrossBlockElements(); 2012 setGeckoEditingOptions(); 2013 addBrAfterLastLinks(); 2014 removeGhostSelection(); 2015 } 2016 }; 2017 (function(tinymce) { 2018 var namedEntities, baseEntities, reverseEntities, 2019 attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 2020 textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 2021 rawCharsRegExp = /[<>&\"\']/g, 2022 entityRegExp = /&(#x|#)?([\w]+);/g, 2023 asciiMap = { 2024 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020", 2025 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152", 2026 142 : "\u017D", 145 : "\u2018", 146 : "\u2019", 147 : "\u201C", 148 : "\u201D", 149 : "\u2022", 2027 150 : "\u2013", 151 : "\u2014", 152 : "\u02DC", 153 : "\u2122", 154 : "\u0161", 155 : "\u203A", 2028 156 : "\u0153", 158 : "\u017E", 159 : "\u0178" 2029 }; 2030 2031 // Raw entities 2032 baseEntities = { 2033 '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code 2034 "'" : ''', 2035 '<' : '<', 2036 '>' : '>', 2037 '&' : '&' 2038 }; 2039 2040 // Reverse lookup table for raw entities 2041 reverseEntities = { 2042 '<' : '<', 2043 '>' : '>', 2044 '&' : '&', 2045 '"' : '"', 2046 ''' : "'" 2047 }; 2048 2049 // Decodes text by using the browser 2050 function nativeDecode(text) { 2051 var elm; 2052 2053 elm = document.createElement("div"); 2054 elm.innerHTML = text; 2055 2056 return elm.textContent || elm.innerText || text; 2057 }; 2058 2059 // Build a two way lookup table for the entities 2060 function buildEntitiesLookup(items, radix) { 2061 var i, chr, entity, lookup = {}; 2062 2063 if (items) { 2064 items = items.split(','); 2065 radix = radix || 10; 2066 2067 // Build entities lookup table 2068 for (i = 0; i < items.length; i += 2) { 2069 chr = String.fromCharCode(parseInt(items[i], radix)); 2070 2071 // Only add non base entities 2072 if (!baseEntities[chr]) { 2073 entity = '&' + items[i + 1] + ';'; 2074 lookup[chr] = entity; 2075 lookup[entity] = chr; 2076 } 2077 } 2078 2079 return lookup; 2080 } 2081 }; 2082 2083 // Unpack entities lookup where the numbers are in radix 32 to reduce the size 2084 namedEntities = buildEntitiesLookup( 2085 '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + 2086 '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + 2087 '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + 2088 '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + 2089 '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + 2090 '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + 2091 '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + 2092 '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + 2093 '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + 2094 '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + 2095 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + 2096 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + 2097 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + 2098 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + 2099 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + 2100 '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + 2101 '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + 2102 '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + 2103 '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + 2104 '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + 2105 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + 2106 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + 2107 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + 2108 '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + 2109 '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32); 2110 2111 tinymce.html = tinymce.html || {}; 2112 2113 tinymce.html.Entities = { 2114 encodeRaw : function(text, attr) { 2115 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2116 return baseEntities[chr] || chr; 2117 }); 2118 }, 2119 2120 encodeAllRaw : function(text) { 2121 return ('' + text).replace(rawCharsRegExp, function(chr) { 2122 return baseEntities[chr] || chr; 2123 }); 2124 }, 2125 2126 encodeNumeric : function(text, attr) { 2127 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2128 // Multi byte sequence convert it to a single entity 2129 if (chr.length > 1) 2130 return '' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; 2131 2132 return baseEntities[chr] || '' + chr.charCodeAt(0) + ';'; 2133 }); 2134 }, 2135 2136 encodeNamed : function(text, attr, entities) { 2137 entities = entities || namedEntities; 2138 2139 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2140 return baseEntities[chr] || entities[chr] || chr; 2141 }); 2142 }, 2143 2144 getEncodeFunc : function(name, entities) { 2145 var Entities = tinymce.html.Entities; 2146 2147 entities = buildEntitiesLookup(entities) || namedEntities; 2148 2149 function encodeNamedAndNumeric(text, attr) { 2150 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2151 return baseEntities[chr] || entities[chr] || '' + chr.charCodeAt(0) + ';' || chr; 2152 }); 2153 }; 2154 2155 function encodeCustomNamed(text, attr) { 2156 return Entities.encodeNamed(text, attr, entities); 2157 }; 2158 2159 // Replace + with , to be compatible with previous TinyMCE versions 2160 name = tinymce.makeMap(name.replace(/\+/g, ',')); 2161 2162 // Named and numeric encoder 2163 if (name.named && name.numeric) 2164 return encodeNamedAndNumeric; 2165 2166 // Named encoder 2167 if (name.named) { 2168 // Custom names 2169 if (entities) 2170 return encodeCustomNamed; 2171 2172 return Entities.encodeNamed; 2173 } 2174 2175 // Numeric 2176 if (name.numeric) 2177 return Entities.encodeNumeric; 2178 2179 // Raw encoder 2180 return Entities.encodeRaw; 2181 }, 2182 2183 decode : function(text) { 2184 return text.replace(entityRegExp, function(all, numeric, value) { 2185 if (numeric) { 2186 value = parseInt(value, numeric.length === 2 ? 16 : 10); 2187 2188 // Support upper UTF 2189 if (value > 0xFFFF) { 2190 value -= 0x10000; 2191 2192 return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF)); 2193 } else 2194 return asciiMap[value] || String.fromCharCode(value); 2195 } 2196 2197 return reverseEntities[all] || namedEntities[all] || nativeDecode(all); 2198 }); 2199 } 2200 }; 2201 })(tinymce); 2202 2203 tinymce.html.Styles = function(settings, schema) { 2204 var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, 2205 urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, 2206 styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, 2207 trimRightRegExp = /\s+$/, 2208 urlColorRegExp = /rgb/, 2209 undef, i, encodingLookup = {}, encodingItems; 2210 2211 settings = settings || {}; 2212 2213 encodingItems = '\\" \\\' \\; \\: ; : \uFEFF'.split(' '); 2214 for (i = 0; i < encodingItems.length; i++) { 2215 encodingLookup[encodingItems[i]] = '\uFEFF' + i; 2216 encodingLookup['\uFEFF' + i] = encodingItems[i]; 2217 } 2218 2219 function toHex(match, r, g, b) { 2220 function hex(val) { 2221 val = parseInt(val).toString(16); 2222 2223 return val.length > 1 ? val : '0' + val; // 0 -> 00 2224 }; 2225 2226 return '#' + hex(r) + hex(g) + hex(b); 2227 }; 2228 2229 return { 2230 toHex : function(color) { 2231 return color.replace(rgbRegExp, toHex); 2232 }, 2233 2234 parse : function(css) { 2235 var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope || this; 2236 2237 function compress(prefix, suffix) { 2238 var top, right, bottom, left; 2239 2240 // Get values and check it it needs compressing 2241 top = styles[prefix + '-top' + suffix]; 2242 if (!top) 2243 return; 2244 2245 right = styles[prefix + '-right' + suffix]; 2246 if (top != right) 2247 return; 2248 2249 bottom = styles[prefix + '-bottom' + suffix]; 2250 if (right != bottom) 2251 return; 2252 2253 left = styles[prefix + '-left' + suffix]; 2254 if (bottom != left) 2255 return; 2256 2257 // Compress 2258 styles[prefix + suffix] = left; 2259 delete styles[prefix + '-top' + suffix]; 2260 delete styles[prefix + '-right' + suffix]; 2261 delete styles[prefix + '-bottom' + suffix]; 2262 delete styles[prefix + '-left' + suffix]; 2263 }; 2264 2265 function canCompress(key) { 2266 var value = styles[key], i; 2267 2268 if (!value || value.indexOf(' ') < 0) 2269 return; 2270 2271 value = value.split(' '); 2272 i = value.length; 2273 while (i--) { 2274 if (value[i] !== value[0]) 2275 return false; 2276 } 2277 2278 styles[key] = value[0]; 2279 2280 return true; 2281 }; 2282 2283 function compress2(target, a, b, c) { 2284 if (!canCompress(a)) 2285 return; 2286 2287 if (!canCompress(b)) 2288 return; 2289 2290 if (!canCompress(c)) 2291 return; 2292 2293 // Compress 2294 styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; 2295 delete styles[a]; 2296 delete styles[b]; 2297 delete styles[c]; 2298 }; 2299 2300 // Encodes the specified string by replacing all \" \' ; : with _<num> 2301 function encode(str) { 2302 isEncoded = true; 2303 2304 return encodingLookup[str]; 2305 }; 2306 2307 // Decodes the specified string by replacing all _<num> with it's original value \" \' etc 2308 // It will also decode the \" \' if keep_slashes is set to fale or omitted 2309 function decode(str, keep_slashes) { 2310 if (isEncoded) { 2311 str = str.replace(/\uFEFF[0-9]/g, function(str) { 2312 return encodingLookup[str]; 2313 }); 2314 } 2315 2316 if (!keep_slashes) 2317 str = str.replace(/\\([\'\";:])/g, "$1"); 2318 2319 return str; 2320 }; 2321 2322 function processUrl(match, url, url2, url3, str, str2) { 2323 str = str || str2; 2324 2325 if (str) { 2326 str = decode(str); 2327 2328 // Force strings into single quote format 2329 return "'" + str.replace(/\'/g, "\\'") + "'"; 2330 } 2331 2332 url = decode(url || url2 || url3); 2333 2334 // Convert the URL to relative/absolute depending on config 2335 if (urlConverter) 2336 url = urlConverter.call(urlConverterScope, url, 'style'); 2337 2338 // Output new URL format 2339 return "url('" + url.replace(/\'/g, "\\'") + "')"; 2340 }; 2341 2342 if (css) { 2343 // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing 2344 css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { 2345 return str.replace(/[;:]/g, encode); 2346 }); 2347 2348 // Parse styles 2349 while (matches = styleRegExp.exec(css)) { 2350 name = matches[1].replace(trimRightRegExp, '').toLowerCase(); 2351 value = matches[2].replace(trimRightRegExp, ''); 2352 2353 if (name && value.length > 0) { 2354 // Opera will produce 700 instead of bold in their style values 2355 if (name === 'font-weight' && value === '700') 2356 value = 'bold'; 2357 else if (name === 'color' || name === 'background-color') // Lowercase colors like RED 2358 value = value.toLowerCase(); 2359 2360 // Convert RGB colors to HEX 2361 value = value.replace(rgbRegExp, toHex); 2362 2363 // Convert URLs and force them into url('value') format 2364 value = value.replace(urlOrStrRegExp, processUrl); 2365 styles[name] = isEncoded ? decode(value, true) : value; 2366 } 2367 2368 styleRegExp.lastIndex = matches.index + matches[0].length; 2369 } 2370 2371 // Compress the styles to reduce it's size for example IE will expand styles 2372 compress("border", ""); 2373 compress("border", "-width"); 2374 compress("border", "-color"); 2375 compress("border", "-style"); 2376 compress("padding", ""); 2377 compress("margin", ""); 2378 compress2('border', 'border-width', 'border-style', 'border-color'); 2379 2380 // Remove pointless border, IE produces these 2381 if (styles.border === 'medium none') 2382 delete styles.border; 2383 } 2384 2385 return styles; 2386 }, 2387 2388 serialize : function(styles, element_name) { 2389 var css = '', name, value; 2390 2391 function serializeStyles(name) { 2392 var styleList, i, l, value; 2393 2394 styleList = schema.styles[name]; 2395 if (styleList) { 2396 for (i = 0, l = styleList.length; i < l; i++) { 2397 name = styleList[i]; 2398 value = styles[name]; 2399 2400 if (value !== undef && value.length > 0) 2401 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 2402 } 2403 } 2404 }; 2405 2406 // Serialize styles according to schema 2407 if (element_name && schema && schema.styles) { 2408 // Serialize global styles and element specific styles 2409 serializeStyles('*'); 2410 serializeStyles(element_name); 2411 } else { 2412 // Output the styles in the order they are inside the object 2413 for (name in styles) { 2414 value = styles[name]; 2415 2416 if (value !== undef && value.length > 0) 2417 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 2418 } 2419 } 2420 2421 return css; 2422 } 2423 }; 2424 }; 2425 2426 (function(tinymce) { 2427 var mapCache = {}, makeMap = tinymce.makeMap, each = tinymce.each; 2428 2429 function split(str, delim) { 2430 return str.split(delim || ','); 2431 }; 2432 2433 function unpack(lookup, data) { 2434 var key, elements = {}; 2435 2436 function replace(value) { 2437 return value.replace(/[A-Z]+/g, function(key) { 2438 return replace(lookup[key]); 2439 }); 2440 }; 2441 2442 // Unpack lookup 2443 for (key in lookup) { 2444 if (lookup.hasOwnProperty(key)) 2445 lookup[key] = replace(lookup[key]); 2446 } 2447 2448 // Unpack and parse data into object map 2449 replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]\[([^\]]*)\]/g, function(str, name, attributes, children) { 2450 attributes = split(attributes, '|'); 2451 2452 elements[name] = { 2453 attributes : makeMap(attributes), 2454 attributesOrder : attributes, 2455 children : makeMap(children, '|', {'#comment' : {}}) 2456 } 2457 }); 2458 2459 return elements; 2460 }; 2461 2462 function getHTML5() { 2463 var html5 = mapCache.html5; 2464 2465 if (!html5) { 2466 html5 = mapCache.html5 = unpack({ 2467 A : 'id|accesskey|class|dir|draggable|item|hidden|itemprop|role|spellcheck|style|subject|title|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', 2468 B : '#|a|abbr|area|audio|b|bdo|br|button|canvas|cite|code|command|datalist|del|dfn|em|embed|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|meta|' + 2469 'meter|noscript|object|output|progress|q|ruby|samp|script|select|small|span|strong|sub|sup|svg|textarea|time|var|video|wbr', 2470 C : '#|a|abbr|area|address|article|aside|audio|b|bdo|blockquote|br|button|canvas|cite|code|command|datalist|del|details|dfn|dialog|div|dl|em|embed|fieldset|' + 2471 'figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|menu|meta|meter|nav|noscript|ol|object|output|' + 2472 'p|pre|progress|q|ruby|samp|script|section|select|small|span|strong|style|sub|sup|svg|table|textarea|time|ul|var|video' 2473 }, 'html[A|manifest][body|head]' + 2474 'head[A][base|command|link|meta|noscript|script|style|title]' + 2475 'title[A][#]' + 2476 'base[A|href|target][]' + 2477 'link[A|href|rel|media|type|sizes][]' + 2478 'meta[A|http-equiv|name|content|charset][]' + 2479 'style[A|type|media|scoped][#]' + 2480 'script[A|charset|type|src|defer|async][#]' + 2481 'noscript[A][C]' + 2482 'body[A][C]' + 2483 'section[A][C]' + 2484 'nav[A][C]' + 2485 'article[A][C]' + 2486 'aside[A][C]' + 2487 'h1[A][B]' + 2488 'h2[A][B]' + 2489 'h3[A][B]' + 2490 'h4[A][B]' + 2491 'h5[A][B]' + 2492 'h6[A][B]' + 2493 'hgroup[A][h1|h2|h3|h4|h5|h6]' + 2494 'header[A][C]' + 2495 'footer[A][C]' + 2496 'address[A][C]' + 2497 'p[A][B]' + 2498 'br[A][]' + 2499 'pre[A][B]' + 2500 'dialog[A][dd|dt]' + 2501 'blockquote[A|cite][C]' + 2502 'ol[A|start|reversed][li]' + 2503 'ul[A][li]' + 2504 'li[A|value][C]' + 2505 'dl[A][dd|dt]' + 2506 'dt[A][B]' + 2507 'dd[A][C]' + 2508 'a[A|href|target|ping|rel|media|type][B]' + 2509 'em[A][B]' + 2510 'strong[A][B]' + 2511 'small[A][B]' + 2512 'cite[A][B]' + 2513 'q[A|cite][B]' + 2514 'dfn[A][B]' + 2515 'abbr[A][B]' + 2516 'code[A][B]' + 2517 'var[A][B]' + 2518 'samp[A][B]' + 2519 'kbd[A][B]' + 2520 'sub[A][B]' + 2521 'sup[A][B]' + 2522 'i[A][B]' + 2523 'b[A][B]' + 2524 'mark[A][B]' + 2525 'progress[A|value|max][B]' + 2526 'meter[A|value|min|max|low|high|optimum][B]' + 2527 'time[A|datetime][B]' + 2528 'ruby[A][B|rt|rp]' + 2529 'rt[A][B]' + 2530 'rp[A][B]' + 2531 'bdo[A][B]' + 2532 'span[A][B]' + 2533 'ins[A|cite|datetime][B]' + 2534 'del[A|cite|datetime][B]' + 2535 'figure[A][C|legend|figcaption]' + 2536 'figcaption[A][C]' + 2537 'img[A|alt|src|height|width|usemap|ismap][]' + 2538 'iframe[A|name|src|height|width|sandbox|seamless][]' + 2539 'embed[A|src|height|width|type][]' + 2540 'object[A|data|type|height|width|usemap|name|form|classid][param]' + 2541 'param[A|name|value][]' + 2542 'details[A|open][C|legend]' + 2543 'command[A|type|label|icon|disabled|checked|radiogroup][]' + 2544 'menu[A|type|label][C|li]' + 2545 'legend[A][C|B]' + 2546 'div[A][C]' + 2547 'source[A|src|type|media][]' + 2548 'audio[A|src|autobuffer|autoplay|loop|controls][source]' + 2549 'video[A|src|autobuffer|autoplay|loop|controls|width|height|poster][source]' + 2550 'hr[A][]' + 2551 'form[A|accept-charset|action|autocomplete|enctype|method|name|novalidate|target][C]' + 2552 'fieldset[A|disabled|form|name][C|legend]' + 2553 'label[A|form|for][B]' + 2554 'input[A|type|accept|alt|autocomplete|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|' + 2555 'multiple|pattern|placeholder|readonly|required|size|src|step|width|files|value|name][]' + 2556 'button[A|autofocus|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|name|value|type][B]' + 2557 'select[A|autofocus|disabled|form|multiple|name|size][option|optgroup]' + 2558 'datalist[A][B|option]' + 2559 'optgroup[A|disabled|label][option]' + 2560 'option[A|disabled|selected|label|value][]' + 2561 'textarea[A|autofocus|disabled|form|maxlength|name|placeholder|readonly|required|rows|cols|wrap][]' + 2562 'keygen[A|autofocus|challenge|disabled|form|keytype|name][]' + 2563 'output[A|for|form|name][B]' + 2564 'canvas[A|width|height][]' + 2565 'map[A|name][B|C]' + 2566 'area[A|shape|coords|href|alt|target|media|rel|ping|type][]' + 2567 'mathml[A][]' + 2568 'svg[A][]' + 2569 'table[A|border][caption|colgroup|thead|tfoot|tbody|tr]' + 2570 'caption[A][C]' + 2571 'colgroup[A|span][col]' + 2572 'col[A|span][]' + 2573 'thead[A][tr]' + 2574 'tfoot[A][tr]' + 2575 'tbody[A][tr]' + 2576 'tr[A][th|td]' + 2577 'th[A|headers|rowspan|colspan|scope][B]' + 2578 'td[A|headers|rowspan|colspan][C]' + 2579 'wbr[A][]' 2580 ); 2581 } 2582 2583 return html5; 2584 }; 2585 2586 function getHTML4() { 2587 var html4 = mapCache.html4; 2588 2589 if (!html4) { 2590 // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size 2591 html4 = mapCache.html4 = unpack({ 2592 Z : 'H|K|N|O|P', 2593 Y : 'X|form|R|Q', 2594 ZG : 'E|span|width|align|char|charoff|valign', 2595 X : 'p|T|div|U|W|isindex|fieldset|table', 2596 ZF : 'E|align|char|charoff|valign', 2597 W : 'pre|hr|blockquote|address|center|noframes', 2598 ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height', 2599 ZD : '[E][S]', 2600 U : 'ul|ol|dl|menu|dir', 2601 ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q', 2602 T : 'h1|h2|h3|h4|h5|h6', 2603 ZB : 'X|S|Q', 2604 S : 'R|P', 2605 ZA : 'a|G|J|M|O|P', 2606 R : 'a|H|K|N|O', 2607 Q : 'noscript|P', 2608 P : 'ins|del|script', 2609 O : 'input|select|textarea|label|button', 2610 N : 'M|L', 2611 M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym', 2612 L : 'sub|sup', 2613 K : 'J|I', 2614 J : 'tt|i|b|u|s|strike', 2615 I : 'big|small|font|basefont', 2616 H : 'G|F', 2617 G : 'br|span|bdo', 2618 F : 'object|applet|img|map|iframe', 2619 E : 'A|B|C', 2620 D : 'accesskey|tabindex|onfocus|onblur', 2621 C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', 2622 B : 'lang|xml:lang|dir', 2623 A : 'id|class|style|title' 2624 }, 'script[id|charset|type|language|src|defer|xml:space][]' + 2625 'style[B|id|type|media|title|xml:space][]' + 2626 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' + 2627 'param[id|name|value|valuetype|type][]' + 2628 'p[E|align][#|S]' + 2629 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' + 2630 'br[A|clear][]' + 2631 'span[E][#|S]' + 2632 'bdo[A|C|B][#|S]' + 2633 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' + 2634 'h1[E|align][#|S]' + 2635 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' + 2636 'map[B|C|A|name][X|form|Q|area]' + 2637 'h2[E|align][#|S]' + 2638 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' + 2639 'h3[E|align][#|S]' + 2640 'tt[E][#|S]' + 2641 'i[E][#|S]' + 2642 'b[E][#|S]' + 2643 'u[E][#|S]' + 2644 's[E][#|S]' + 2645 'strike[E][#|S]' + 2646 'big[E][#|S]' + 2647 'small[E][#|S]' + 2648 'font[A|B|size|color|face][#|S]' + 2649 'basefont[id|size|color|face][]' + 2650 'em[E][#|S]' + 2651 'strong[E][#|S]' + 2652 'dfn[E][#|S]' + 2653 'code[E][#|S]' + 2654 'q[E|cite][#|S]' + 2655 'samp[E][#|S]' + 2656 'kbd[E][#|S]' + 2657 'var[E][#|S]' + 2658 'cite[E][#|S]' + 2659 'abbr[E][#|S]' + 2660 'acronym[E][#|S]' + 2661 'sub[E][#|S]' + 2662 'sup[E][#|S]' + 2663 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' + 2664 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' + 2665 'optgroup[E|disabled|label][option]' + 2666 'option[E|selected|disabled|label|value][]' + 2667 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' + 2668 'label[E|for|accesskey|onfocus|onblur][#|S]' + 2669 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' + 2670 'h4[E|align][#|S]' + 2671 'ins[E|cite|datetime][#|Y]' + 2672 'h5[E|align][#|S]' + 2673 'del[E|cite|datetime][#|Y]' + 2674 'h6[E|align][#|S]' + 2675 'div[E|align][#|Y]' + 2676 'ul[E|type|compact][li]' + 2677 'li[E|type|value][#|Y]' + 2678 'ol[E|type|compact|start][li]' + 2679 'dl[E|compact][dt|dd]' + 2680 'dt[E][#|S]' + 2681 'dd[E][#|Y]' + 2682 'menu[E|compact][li]' + 2683 'dir[E|compact][li]' + 2684 'pre[E|width|xml:space][#|ZA]' + 2685 'hr[E|align|noshade|size|width][]' + 2686 'blockquote[E|cite][#|Y]' + 2687 'address[E][#|S|p]' + 2688 'center[E][#|Y]' + 2689 'noframes[E][#|Y]' + 2690 'isindex[A|B|prompt][]' + 2691 'fieldset[E][#|legend|Y]' + 2692 'legend[E|accesskey|align][#|S]' + 2693 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' + 2694 'caption[E|align][#|S]' + 2695 'col[ZG][]' + 2696 'colgroup[ZG][col]' + 2697 'thead[ZF][tr]' + 2698 'tr[ZF|bgcolor][th|td]' + 2699 'th[E|ZE][#|Y]' + 2700 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' + 2701 'noscript[E][#|Y]' + 2702 'td[E|ZE][#|Y]' + 2703 'tfoot[ZF][tr]' + 2704 'tbody[ZF][tr]' + 2705 'area[E|D|shape|coords|href|nohref|alt|target][]' + 2706 'base[id|href|target][]' + 2707 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]' 2708 ); 2709 } 2710 2711 return html4; 2712 }; 2713 2714 tinymce.html.Schema = function(settings) { 2715 var self = this, elements = {}, children = {}, patternElements = [], validStyles, schemaItems; 2716 var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, blockElementsMap, nonEmptyElementsMap, customElementsMap = {}; 2717 2718 // Creates an lookup table map object for the specified option or the default value 2719 function createLookupTable(option, default_value, extend) { 2720 var value = settings[option]; 2721 2722 if (!value) { 2723 // Get cached default map or make it if needed 2724 value = mapCache[option]; 2725 2726 if (!value) { 2727 value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' ')); 2728 value = tinymce.extend(value, extend); 2729 2730 mapCache[option] = value; 2731 } 2732 } else { 2733 // Create custom map 2734 value = makeMap(value, ',', makeMap(value.toUpperCase(), ' ')); 2735 } 2736 2737 return value; 2738 }; 2739 2740 settings = settings || {}; 2741 schemaItems = settings.schema == "html5" ? getHTML5() : getHTML4(); 2742 2743 // Allow all elements and attributes if verify_html is set to false 2744 if (settings.verify_html === false) 2745 settings.valid_elements = '*[*]'; 2746 2747 // Build styles list 2748 if (settings.valid_styles) { 2749 validStyles = {}; 2750 2751 // Convert styles into a rule list 2752 each(settings.valid_styles, function(value, key) { 2753 validStyles[key] = tinymce.explode(value); 2754 }); 2755 } 2756 2757 // Setup map objects 2758 whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script style textarea'); 2759 selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); 2760 shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link meta param embed source wbr'); 2761 boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize noshade nowrap readonly selected autoplay loop controls'); 2762 nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object', shortEndedElementsMap); 2763 blockElementsMap = createLookupTable('block_elements', 'h1 h2 h3 h4 h5 h6 hr p div address pre form table tbody thead tfoot ' + 2764 'th tr td li ol ul caption blockquote center dl dt dd dir fieldset ' + 2765 'noscript menu isindex samp header footer article section hgroup aside nav figure option datalist select optgroup'); 2766 2767 // Converts a wildcard expression string to a regexp for example *a will become /.*a/. 2768 function patternToRegExp(str) { 2769 return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); 2770 }; 2771 2772 // Parses the specified valid_elements string and adds to the current rules 2773 // This function is a bit hard to read since it's heavily optimized for speed 2774 function addValidElements(valid_elements) { 2775 var ei, el, ai, al, yl, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, 2776 prefix, outputName, globalAttributes, globalAttributesOrder, transElement, key, childKey, value, 2777 elementRuleRegExp = /^([#+\-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/, 2778 attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, 2779 hasPatternsRegExp = /[*?+]/; 2780 2781 if (valid_elements) { 2782 // Split valid elements into an array with rules 2783 valid_elements = split(valid_elements); 2784 2785 if (elements['@']) { 2786 globalAttributes = elements['@'].attributes; 2787 globalAttributesOrder = elements['@'].attributesOrder; 2788 } 2789 2790 // Loop all rules 2791 for (ei = 0, el = valid_elements.length; ei < el; ei++) { 2792 // Parse element rule 2793 matches = elementRuleRegExp.exec(valid_elements[ei]); 2794 if (matches) { 2795 // Setup local names for matches 2796 prefix = matches[1]; 2797 elementName = matches[2]; 2798 outputName = matches[3]; 2799 attrData = matches[4]; 2800 2801 // Create new attributes and attributesOrder 2802 attributes = {}; 2803 attributesOrder = []; 2804 2805 // Create the new element 2806 element = { 2807 attributes : attributes, 2808 attributesOrder : attributesOrder 2809 }; 2810 2811 // Padd empty elements prefix 2812 if (prefix === '#') 2813 element.paddEmpty = true; 2814 2815 // Remove empty elements prefix 2816 if (prefix === '-') 2817 element.removeEmpty = true; 2818 2819 // Copy attributes from global rule into current rule 2820 if (globalAttributes) { 2821 for (key in globalAttributes) 2822 attributes[key] = globalAttributes[key]; 2823 2824 attributesOrder.push.apply(attributesOrder, globalAttributesOrder); 2825 } 2826 2827 // Attributes defined 2828 if (attrData) { 2829 attrData = split(attrData, '|'); 2830 for (ai = 0, al = attrData.length; ai < al; ai++) { 2831 matches = attrRuleRegExp.exec(attrData[ai]); 2832 if (matches) { 2833 attr = {}; 2834 attrType = matches[1]; 2835 attrName = matches[2].replace(/::/g, ':'); 2836 prefix = matches[3]; 2837 value = matches[4]; 2838 2839 // Required 2840 if (attrType === '!') { 2841 element.attributesRequired = element.attributesRequired || []; 2842 element.attributesRequired.push(attrName); 2843 attr.required = true; 2844 } 2845 2846 // Denied from global 2847 if (attrType === '-') { 2848 delete attributes[attrName]; 2849 attributesOrder.splice(tinymce.inArray(attributesOrder, attrName), 1); 2850 continue; 2851 } 2852 2853 // Default value 2854 if (prefix) { 2855 // Default value 2856 if (prefix === '=') { 2857 element.attributesDefault = element.attributesDefault || []; 2858 element.attributesDefault.push({name: attrName, value: value}); 2859 attr.defaultValue = value; 2860 } 2861 2862 // Forced value 2863 if (prefix === ':') { 2864 element.attributesForced = element.attributesForced || []; 2865 element.attributesForced.push({name: attrName, value: value}); 2866 attr.forcedValue = value; 2867 } 2868 2869 // Required values 2870 if (prefix === '<') 2871 attr.validValues = makeMap(value, '?'); 2872 } 2873 2874 // Check for attribute patterns 2875 if (hasPatternsRegExp.test(attrName)) { 2876 element.attributePatterns = element.attributePatterns || []; 2877 attr.pattern = patternToRegExp(attrName); 2878 element.attributePatterns.push(attr); 2879 } else { 2880 // Add attribute to order list if it doesn't already exist 2881 if (!attributes[attrName]) 2882 attributesOrder.push(attrName); 2883 2884 attributes[attrName] = attr; 2885 } 2886 } 2887 } 2888 } 2889 2890 // Global rule, store away these for later usage 2891 if (!globalAttributes && elementName == '@') { 2892 globalAttributes = attributes; 2893 globalAttributesOrder = attributesOrder; 2894 } 2895 2896 // Handle substitute elements such as b/strong 2897 if (outputName) { 2898 element.outputName = elementName; 2899 elements[outputName] = element; 2900 } 2901 2902 // Add pattern or exact element 2903 if (hasPatternsRegExp.test(elementName)) { 2904 element.pattern = patternToRegExp(elementName); 2905 patternElements.push(element); 2906 } else 2907 elements[elementName] = element; 2908 } 2909 } 2910 } 2911 }; 2912 2913 function setValidElements(valid_elements) { 2914 elements = {}; 2915 patternElements = []; 2916 2917 addValidElements(valid_elements); 2918 2919 each(schemaItems, function(element, name) { 2920 children[name] = element.children; 2921 }); 2922 }; 2923 2924 // Adds custom non HTML elements to the schema 2925 function addCustomElements(custom_elements) { 2926 var customElementRegExp = /^(~)?(.+)$/; 2927 2928 if (custom_elements) { 2929 each(split(custom_elements), function(rule) { 2930 var matches = customElementRegExp.exec(rule), 2931 inline = matches[1] === '~', 2932 cloneName = inline ? 'span' : 'div', 2933 name = matches[2]; 2934 2935 children[name] = children[cloneName]; 2936 customElementsMap[name] = cloneName; 2937 2938 // If it's not marked as inline then add it to valid block elements 2939 if (!inline) 2940 blockElementsMap[name] = {}; 2941 2942 // Add custom elements at span/div positions 2943 each(children, function(element, child) { 2944 if (element[cloneName]) 2945 element[name] = element[cloneName]; 2946 }); 2947 }); 2948 } 2949 }; 2950 2951 // Adds valid children to the schema object 2952 function addValidChildren(valid_children) { 2953 var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; 2954 2955 if (valid_children) { 2956 each(split(valid_children), function(rule) { 2957 var matches = childRuleRegExp.exec(rule), parent, prefix; 2958 2959 if (matches) { 2960 prefix = matches[1]; 2961 2962 // Add/remove items from default 2963 if (prefix) 2964 parent = children[matches[2]]; 2965 else 2966 parent = children[matches[2]] = {'#comment' : {}}; 2967 2968 parent = children[matches[2]]; 2969 2970 each(split(matches[3], '|'), function(child) { 2971 if (prefix === '-') 2972 delete parent[child]; 2973 else 2974 parent[child] = {}; 2975 }); 2976 } 2977 }); 2978 } 2979 }; 2980 2981 function getElementRule(name) { 2982 var element = elements[name], i; 2983 2984 // Exact match found 2985 if (element) 2986 return element; 2987 2988 // No exact match then try the patterns 2989 i = patternElements.length; 2990 while (i--) { 2991 element = patternElements[i]; 2992 2993 if (element.pattern.test(name)) 2994 return element; 2995 } 2996 }; 2997 2998 if (!settings.valid_elements) { 2999 // No valid elements defined then clone the elements from the schema spec 3000 each(schemaItems, function(element, name) { 3001 elements[name] = { 3002 attributes : element.attributes, 3003 attributesOrder : element.attributesOrder 3004 }; 3005 3006 children[name] = element.children; 3007 }); 3008 3009 // Switch these on HTML4 3010 if (settings.schema != "html5") { 3011 each(split('strong/b,em/i'), function(item) { 3012 item = split(item, '/'); 3013 elements[item[1]].outputName = item[0]; 3014 }); 3015 } 3016 3017 // Add default alt attribute for images 3018 elements.img.attributesDefault = [{name: 'alt', value: ''}]; 3019 3020 // Remove these if they are empty by default 3021 each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr,strong,em,b,i'), function(name) { 3022 if (elements[name]) { 3023 elements[name].removeEmpty = true; 3024 } 3025 }); 3026 3027 // Padd these by default 3028 each(split('p,h1,h2,h3,h4,h5,h6,th,td,pre,div,address,caption'), function(name) { 3029 elements[name].paddEmpty = true; 3030 }); 3031 } else 3032 setValidElements(settings.valid_elements); 3033 3034 addCustomElements(settings.custom_elements); 3035 addValidChildren(settings.valid_children); 3036 addValidElements(settings.extended_valid_elements); 3037 3038 // Todo: Remove this when we fix list handling to be valid 3039 addValidChildren('+ol[ul|ol],+ul[ul|ol]'); 3040 3041 // Delete invalid elements 3042 if (settings.invalid_elements) { 3043 tinymce.each(tinymce.explode(settings.invalid_elements), function(item) { 3044 if (elements[item]) 3045 delete elements[item]; 3046 }); 3047 } 3048 3049 // If the user didn't allow span only allow internal spans 3050 if (!getElementRule('span')) 3051 addValidElements('span[!data-mce-type|*]'); 3052 3053 self.children = children; 3054 3055 self.styles = validStyles; 3056 3057 self.getBoolAttrs = function() { 3058 return boolAttrMap; 3059 }; 3060 3061 self.getBlockElements = function() { 3062 return blockElementsMap; 3063 }; 3064 3065 self.getShortEndedElements = function() { 3066 return shortEndedElementsMap; 3067 }; 3068 3069 self.getSelfClosingElements = function() { 3070 return selfClosingElementsMap; 3071 }; 3072 3073 self.getNonEmptyElements = function() { 3074 return nonEmptyElementsMap; 3075 }; 3076 3077 self.getWhiteSpaceElements = function() { 3078 return whiteSpaceElementsMap; 3079 }; 3080 3081 self.isValidChild = function(name, child) { 3082 var parent = children[name]; 3083 3084 return !!(parent && parent[child]); 3085 }; 3086 3087 self.isValid = function(name, attr) { 3088 var attrPatterns, i, rule = getElementRule(name); 3089 3090 // Check if it's a valid element 3091 if (rule) { 3092 if (attr) { 3093 // Check if attribute name exists 3094 if (rule.attributes[attr]) { 3095 return true; 3096 } 3097 3098 // Check if attribute matches a regexp pattern 3099 attrPatterns = rule.attributePatterns; 3100 if (attrPatterns) { 3101 i = attrPatterns.length; 3102 while (i--) { 3103 if (attrPatterns[i].pattern.test(name)) { 3104 return true; 3105 } 3106 } 3107 } 3108 } else { 3109 return true; 3110 } 3111 } 3112 3113 // No match 3114 return false; 3115 }; 3116 3117 self.getElementRule = getElementRule; 3118 3119 self.getCustomElements = function() { 3120 return customElementsMap; 3121 }; 3122 3123 self.addValidElements = addValidElements; 3124 3125 self.setValidElements = setValidElements; 3126 3127 self.addCustomElements = addCustomElements; 3128 3129 self.addValidChildren = addValidChildren; 3130 }; 3131 })(tinymce); 3132 3133 (function(tinymce) { 3134 tinymce.html.SaxParser = function(settings, schema) { 3135 var self = this, noop = function() {}; 3136 3137 settings = settings || {}; 3138 self.schema = schema = schema || new tinymce.html.Schema(); 3139 3140 if (settings.fix_self_closing !== false) 3141 settings.fix_self_closing = true; 3142 3143 // Add handler functions from settings and setup default handlers 3144 tinymce.each('comment cdata text start end pi doctype'.split(' '), function(name) { 3145 if (name) 3146 self[name] = settings[name] || noop; 3147 }); 3148 3149 self.parse = function(html) { 3150 var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name, isInternalElement, removeInternalElements, 3151 shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue, invalidPrefixRegExp, 3152 validAttributesMap, validAttributePatterns, attributesRequired, attributesDefault, attributesForced, selfClosing, 3153 tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing, isIE; 3154 3155 function processEndTag(name) { 3156 var pos, i; 3157 3158 // Find position of parent of the same type 3159 pos = stack.length; 3160 while (pos--) { 3161 if (stack[pos].name === name) 3162 break; 3163 } 3164 3165 // Found parent 3166 if (pos >= 0) { 3167 // Close all the open elements 3168 for (i = stack.length - 1; i >= pos; i--) { 3169 name = stack[i]; 3170 3171 if (name.valid) 3172 self.end(name.name); 3173 } 3174 3175 // Remove the open elements from the stack 3176 stack.length = pos; 3177 } 3178 }; 3179 3180 function parseAttribute(match, name, value, val2, val3) { 3181 var attrRule, i; 3182 3183 name = name.toLowerCase(); 3184 value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute 3185 3186 // Validate name and value 3187 if (validate && !isInternalElement && name.indexOf('data-') !== 0) { 3188 attrRule = validAttributesMap[name]; 3189 3190 // Find rule by pattern matching 3191 if (!attrRule && validAttributePatterns) { 3192 i = validAttributePatterns.length; 3193 while (i--) { 3194 attrRule = validAttributePatterns[i]; 3195 if (attrRule.pattern.test(name)) 3196 break; 3197 } 3198 3199 // No rule matched 3200 if (i === -1) 3201 attrRule = null; 3202 } 3203 3204 // No attribute rule found 3205 if (!attrRule) 3206 return; 3207 3208 // Validate value 3209 if (attrRule.validValues && !(value in attrRule.validValues)) 3210 return; 3211 } 3212 3213 // Add attribute to list and map 3214 attrList.map[name] = value; 3215 attrList.push({ 3216 name: name, 3217 value: value 3218 }); 3219 }; 3220 3221 // Precompile RegExps and map objects 3222 tokenRegExp = new RegExp('<(?:' + 3223 '(?:!--([\\w\\W]*?)-->)|' + // Comment 3224 '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA 3225 '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE 3226 '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI 3227 '(?:\\/([^>]+)>)|' + // End element 3228 '(?:([A-Za-z0-9\\-\\:]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element 3229 ')', 'g'); 3230 3231 attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g; 3232 specialElements = { 3233 'script' : /<\/script[^>]*>/gi, 3234 'style' : /<\/style[^>]*>/gi, 3235 'noscript' : /<\/noscript[^>]*>/gi 3236 }; 3237 3238 // Setup lookup tables for empty elements and boolean attributes 3239 shortEndedElements = schema.getShortEndedElements(); 3240 selfClosing = settings.self_closing_elements || schema.getSelfClosingElements(); 3241 fillAttrsMap = schema.getBoolAttrs(); 3242 validate = settings.validate; 3243 removeInternalElements = settings.remove_internals; 3244 fixSelfClosing = settings.fix_self_closing; 3245 isIE = tinymce.isIE; 3246 invalidPrefixRegExp = /^:/; 3247 3248 while (matches = tokenRegExp.exec(html)) { 3249 // Text 3250 if (index < matches.index) 3251 self.text(decode(html.substr(index, matches.index - index))); 3252 3253 if (value = matches[6]) { // End element 3254 value = value.toLowerCase(); 3255 3256 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 3257 if (isIE && invalidPrefixRegExp.test(value)) 3258 value = value.substr(1); 3259 3260 processEndTag(value); 3261 } else if (value = matches[7]) { // Start element 3262 value = value.toLowerCase(); 3263 3264 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 3265 if (isIE && invalidPrefixRegExp.test(value)) 3266 value = value.substr(1); 3267 3268 isShortEnded = value in shortEndedElements; 3269 3270 // Is self closing tag for example an <li> after an open <li> 3271 if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) 3272 processEndTag(value); 3273 3274 // Validate element 3275 if (!validate || (elementRule = schema.getElementRule(value))) { 3276 isValidElement = true; 3277 3278 // Grab attributes map and patters when validation is enabled 3279 if (validate) { 3280 validAttributesMap = elementRule.attributes; 3281 validAttributePatterns = elementRule.attributePatterns; 3282 } 3283 3284 // Parse attributes 3285 if (attribsValue = matches[8]) { 3286 isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element 3287 3288 // If the element has internal attributes then remove it if we are told to do so 3289 if (isInternalElement && removeInternalElements) 3290 isValidElement = false; 3291 3292 attrList = []; 3293 attrList.map = {}; 3294 3295 attribsValue.replace(attrRegExp, parseAttribute); 3296 } else { 3297 attrList = []; 3298 attrList.map = {}; 3299 } 3300 3301 // Process attributes if validation is enabled 3302 if (validate && !isInternalElement) { 3303 attributesRequired = elementRule.attributesRequired; 3304 attributesDefault = elementRule.attributesDefault; 3305 attributesForced = elementRule.attributesForced; 3306 3307 // Handle forced attributes 3308 if (attributesForced) { 3309 i = attributesForced.length; 3310 while (i--) { 3311 attr = attributesForced[i]; 3312 name = attr.name; 3313 attrValue = attr.value; 3314 3315 if (attrValue === '{$uid}') 3316 attrValue = 'mce_' + idCount++; 3317 3318 attrList.map[name] = attrValue; 3319 attrList.push({name: name, value: attrValue}); 3320 } 3321 } 3322 3323 // Handle default attributes 3324 if (attributesDefault) { 3325 i = attributesDefault.length; 3326 while (i--) { 3327 attr = attributesDefault[i]; 3328 name = attr.name; 3329 3330 if (!(name in attrList.map)) { 3331 attrValue = attr.value; 3332 3333 if (attrValue === '{$uid}') 3334 attrValue = 'mce_' + idCount++; 3335 3336 attrList.map[name] = attrValue; 3337 attrList.push({name: name, value: attrValue}); 3338 } 3339 } 3340 } 3341 3342 // Handle required attributes 3343 if (attributesRequired) { 3344 i = attributesRequired.length; 3345 while (i--) { 3346 if (attributesRequired[i] in attrList.map) 3347 break; 3348 } 3349 3350 // None of the required attributes where found 3351 if (i === -1) 3352 isValidElement = false; 3353 } 3354 3355 // Invalidate element if it's marked as bogus 3356 if (attrList.map['data-mce-bogus']) 3357 isValidElement = false; 3358 } 3359 3360 if (isValidElement) 3361 self.start(value, attrList, isShortEnded); 3362 } else 3363 isValidElement = false; 3364 3365 // Treat script, noscript and style a bit different since they may include code that looks like elements 3366 if (endRegExp = specialElements[value]) { 3367 endRegExp.lastIndex = index = matches.index + matches[0].length; 3368 3369 if (matches = endRegExp.exec(html)) { 3370 if (isValidElement) 3371 text = html.substr(index, matches.index - index); 3372 3373 index = matches.index + matches[0].length; 3374 } else { 3375 text = html.substr(index); 3376 index = html.length; 3377 } 3378 3379 if (isValidElement && text.length > 0) 3380 self.text(text, true); 3381 3382 if (isValidElement) 3383 self.end(value); 3384 3385 tokenRegExp.lastIndex = index; 3386 continue; 3387 } 3388 3389 // Push value on to stack 3390 if (!isShortEnded) { 3391 if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) 3392 stack.push({name: value, valid: isValidElement}); 3393 else if (isValidElement) 3394 self.end(value); 3395 } 3396 } else if (value = matches[1]) { // Comment 3397 self.comment(value); 3398 } else if (value = matches[2]) { // CDATA 3399 self.cdata(value); 3400 } else if (value = matches[3]) { // DOCTYPE 3401 self.doctype(value); 3402 } else if (value = matches[4]) { // PI 3403 self.pi(value, matches[5]); 3404 } 3405 3406 index = matches.index + matches[0].length; 3407 } 3408 3409 // Text 3410 if (index < html.length) 3411 self.text(decode(html.substr(index))); 3412 3413 // Close any open elements 3414 for (i = stack.length - 1; i >= 0; i--) { 3415 value = stack[i]; 3416 3417 if (value.valid) 3418 self.end(value.name); 3419 } 3420 }; 3421 } 3422 })(tinymce); 3423 3424 (function(tinymce) { 3425 var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = { 3426 '#text' : 3, 3427 '#comment' : 8, 3428 '#cdata' : 4, 3429 '#pi' : 7, 3430 '#doctype' : 10, 3431 '#document-fragment' : 11 3432 }; 3433 3434 // Walks the tree left/right 3435 function walk(node, root_node, prev) { 3436 var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; 3437 3438 // Walk into nodes if it has a start 3439 if (node[startName]) 3440 return node[startName]; 3441 3442 // Return the sibling if it has one 3443 if (node !== root_node) { 3444 sibling = node[siblingName]; 3445 3446 if (sibling) 3447 return sibling; 3448 3449 // Walk up the parents to look for siblings 3450 for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) { 3451 sibling = parent[siblingName]; 3452 3453 if (sibling) 3454 return sibling; 3455 } 3456 } 3457 }; 3458 3459 function Node(name, type) { 3460 this.name = name; 3461 this.type = type; 3462 3463 if (type === 1) { 3464 this.attributes = []; 3465 this.attributes.map = {}; 3466 } 3467 } 3468 3469 tinymce.extend(Node.prototype, { 3470 replace : function(node) { 3471 var self = this; 3472 3473 if (node.parent) 3474 node.remove(); 3475 3476 self.insert(node, self); 3477 self.remove(); 3478 3479 return self; 3480 }, 3481 3482 attr : function(name, value) { 3483 var self = this, attrs, i, undef; 3484 3485 if (typeof name !== "string") { 3486 for (i in name) 3487 self.attr(i, name[i]); 3488 3489 return self; 3490 } 3491 3492 if (attrs = self.attributes) { 3493 if (value !== undef) { 3494 // Remove attribute 3495 if (value === null) { 3496 if (name in attrs.map) { 3497 delete attrs.map[name]; 3498 3499 i = attrs.length; 3500 while (i--) { 3501 if (attrs[i].name === name) { 3502 attrs = attrs.splice(i, 1); 3503 return self; 3504 } 3505 } 3506 } 3507 3508 return self; 3509 } 3510 3511 // Set attribute 3512 if (name in attrs.map) { 3513 // Set attribute 3514 i = attrs.length; 3515 while (i--) { 3516 if (attrs[i].name === name) { 3517 attrs[i].value = value; 3518 break; 3519 } 3520 } 3521 } else 3522 attrs.push({name: name, value: value}); 3523 3524 attrs.map[name] = value; 3525 3526 return self; 3527 } else { 3528 return attrs.map[name]; 3529 } 3530 } 3531 }, 3532 3533 clone : function() { 3534 var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; 3535 3536 // Clone element attributes 3537 if (selfAttrs = self.attributes) { 3538 cloneAttrs = []; 3539 cloneAttrs.map = {}; 3540 3541 for (i = 0, l = selfAttrs.length; i < l; i++) { 3542 selfAttr = selfAttrs[i]; 3543 3544 // Clone everything except id 3545 if (selfAttr.name !== 'id') { 3546 cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value}; 3547 cloneAttrs.map[selfAttr.name] = selfAttr.value; 3548 } 3549 } 3550 3551 clone.attributes = cloneAttrs; 3552 } 3553 3554 clone.value = self.value; 3555 clone.shortEnded = self.shortEnded; 3556 3557 return clone; 3558 }, 3559 3560 wrap : function(wrapper) { 3561 var self = this; 3562 3563 self.parent.insert(wrapper, self); 3564 wrapper.append(self); 3565 3566 return self; 3567 }, 3568 3569 unwrap : function() { 3570 var self = this, node, next; 3571 3572 for (node = self.firstChild; node; ) { 3573 next = node.next; 3574 self.insert(node, self, true); 3575 node = next; 3576 } 3577 3578 self.remove(); 3579 }, 3580 3581 remove : function() { 3582 var self = this, parent = self.parent, next = self.next, prev = self.prev; 3583 3584 if (parent) { 3585 if (parent.firstChild === self) { 3586 parent.firstChild = next; 3587 3588 if (next) 3589 next.prev = null; 3590 } else { 3591 prev.next = next; 3592 } 3593 3594 if (parent.lastChild === self) { 3595 parent.lastChild = prev; 3596 3597 if (prev) 3598 prev.next = null; 3599 } else { 3600 next.prev = prev; 3601 } 3602 3603 self.parent = self.next = self.prev = null; 3604 } 3605 3606 return self; 3607 }, 3608 3609 append : function(node) { 3610 var self = this, last; 3611 3612 if (node.parent) 3613 node.remove(); 3614 3615 last = self.lastChild; 3616 if (last) { 3617 last.next = node; 3618 node.prev = last; 3619 self.lastChild = node; 3620 } else 3621 self.lastChild = self.firstChild = node; 3622 3623 node.parent = self; 3624 3625 return node; 3626 }, 3627 3628 insert : function(node, ref_node, before) { 3629 var parent; 3630 3631 if (node.parent) 3632 node.remove(); 3633 3634 parent = ref_node.parent || this; 3635 3636 if (before) { 3637 if (ref_node === parent.firstChild) 3638 parent.firstChild = node; 3639 else 3640 ref_node.prev.next = node; 3641 3642 node.prev = ref_node.prev; 3643 node.next = ref_node; 3644 ref_node.prev = node; 3645 } else { 3646 if (ref_node === parent.lastChild) 3647 parent.lastChild = node; 3648 else 3649 ref_node.next.prev = node; 3650 3651 node.next = ref_node.next; 3652 node.prev = ref_node; 3653 ref_node.next = node; 3654 } 3655 3656 node.parent = parent; 3657 3658 return node; 3659 }, 3660 3661 getAll : function(name) { 3662 var self = this, node, collection = []; 3663 3664 for (node = self.firstChild; node; node = walk(node, self)) { 3665 if (node.name === name) 3666 collection.push(node); 3667 } 3668 3669 return collection; 3670 }, 3671 3672 empty : function() { 3673 var self = this, nodes, i, node; 3674 3675 // Remove all children 3676 if (self.firstChild) { 3677 nodes = []; 3678 3679 // Collect the children 3680 for (node = self.firstChild; node; node = walk(node, self)) 3681 nodes.push(node); 3682 3683 // Remove the children 3684 i = nodes.length; 3685 while (i--) { 3686 node = nodes[i]; 3687 node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; 3688 } 3689 } 3690 3691 self.firstChild = self.lastChild = null; 3692 3693 return self; 3694 }, 3695 3696 isEmpty : function(elements) { 3697 var self = this, node = self.firstChild, i, name; 3698 3699 if (node) { 3700 do { 3701 if (node.type === 1) { 3702 // Ignore bogus elements 3703 if (node.attributes.map['data-mce-bogus']) 3704 continue; 3705 3706 // Keep empty elements like <img /> 3707 if (elements[node.name]) 3708 return false; 3709 3710 // Keep elements with data attributes or name attribute like <a name="1"></a> 3711 i = node.attributes.length; 3712 while (i--) { 3713 name = node.attributes[i].name; 3714 if (name === "name" || name.indexOf('data-') === 0) 3715 return false; 3716 } 3717 } 3718 3719 // Keep comments 3720 if (node.type === 8) 3721 return false; 3722 3723 // Keep non whitespace text nodes 3724 if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) 3725 return false; 3726 } while (node = walk(node, self)); 3727 } 3728 3729 return true; 3730 }, 3731 3732 walk : function(prev) { 3733 return walk(this, null, prev); 3734 } 3735 }); 3736 3737 tinymce.extend(Node, { 3738 create : function(name, attrs) { 3739 var node, attrName; 3740 3741 // Create node 3742 node = new Node(name, typeLookup[name] || 1); 3743 3744 // Add attributes if needed 3745 if (attrs) { 3746 for (attrName in attrs) 3747 node.attr(attrName, attrs[attrName]); 3748 } 3749 3750 return node; 3751 } 3752 }); 3753 3754 tinymce.html.Node = Node; 3755 })(tinymce); 3756 3757 (function(tinymce) { 3758 var Node = tinymce.html.Node; 3759 3760 tinymce.html.DomParser = function(settings, schema) { 3761 var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {}; 3762 3763 settings = settings || {}; 3764 settings.validate = "validate" in settings ? settings.validate : true; 3765 settings.root_name = settings.root_name || 'body'; 3766 self.schema = schema = schema || new tinymce.html.Schema(); 3767 3768 function fixInvalidChildren(nodes) { 3769 var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i, 3770 childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode; 3771 3772 nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table'); 3773 nonEmptyElements = schema.getNonEmptyElements(); 3774 3775 for (ni = 0; ni < nodes.length; ni++) { 3776 node = nodes[ni]; 3777 3778 // Already removed 3779 if (!node.parent) 3780 continue; 3781 3782 // Get list of all parent nodes until we find a valid parent to stick the child into 3783 parents = [node]; 3784 for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && !nonSplitableElements[parent.name]; parent = parent.parent) 3785 parents.push(parent); 3786 3787 // Found a suitable parent 3788 if (parent && parents.length > 1) { 3789 // Reverse the array since it makes looping easier 3790 parents.reverse(); 3791 3792 // Clone the related parent and insert that after the moved node 3793 newParent = currentNode = self.filterNode(parents[0].clone()); 3794 3795 // Start cloning and moving children on the left side of the target node 3796 for (i = 0; i < parents.length - 1; i++) { 3797 if (schema.isValidChild(currentNode.name, parents[i].name)) { 3798 tempNode = self.filterNode(parents[i].clone()); 3799 currentNode.append(tempNode); 3800 } else 3801 tempNode = currentNode; 3802 3803 for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) { 3804 nextNode = childNode.next; 3805 tempNode.append(childNode); 3806 childNode = nextNode; 3807 } 3808 3809 currentNode = tempNode; 3810 } 3811 3812 if (!newParent.isEmpty(nonEmptyElements)) { 3813 parent.insert(newParent, parents[0], true); 3814 parent.insert(node, newParent); 3815 } else { 3816 parent.insert(node, parents[0], true); 3817 } 3818 3819 // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p> 3820 parent = parents[0]; 3821 if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') { 3822 parent.empty().remove(); 3823 } 3824 } else if (node.parent) { 3825 // If it's an LI try to find a UL/OL for it or wrap it 3826 if (node.name === 'li') { 3827 sibling = node.prev; 3828 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 3829 sibling.append(node); 3830 continue; 3831 } 3832 3833 sibling = node.next; 3834 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 3835 sibling.insert(node, sibling.firstChild, true); 3836 continue; 3837 } 3838 3839 node.wrap(self.filterNode(new Node('ul', 1))); 3840 continue; 3841 } 3842 3843 // Try wrapping the element in a DIV 3844 if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { 3845 node.wrap(self.filterNode(new Node('div', 1))); 3846 } else { 3847 // We failed wrapping it, then remove or unwrap it 3848 if (node.name === 'style' || node.name === 'script') 3849 node.empty().remove(); 3850 else 3851 node.unwrap(); 3852 } 3853 } 3854 } 3855 }; 3856 3857 self.filterNode = function(node) { 3858 var i, name, list; 3859 3860 // Run element filters 3861 if (name in nodeFilters) { 3862 list = matchedNodes[name]; 3863 3864 if (list) 3865 list.push(node); 3866 else 3867 matchedNodes[name] = [node]; 3868 } 3869 3870 // Run attribute filters 3871 i = attributeFilters.length; 3872 while (i--) { 3873 name = attributeFilters[i].name; 3874 3875 if (name in node.attributes.map) { 3876 list = matchedAttributes[name]; 3877 3878 if (list) 3879 list.push(node); 3880 else 3881 matchedAttributes[name] = [node]; 3882 } 3883 } 3884 3885 return node; 3886 }; 3887 3888 self.addNodeFilter = function(name, callback) { 3889 tinymce.each(tinymce.explode(name), function(name) { 3890 var list = nodeFilters[name]; 3891 3892 if (!list) 3893 nodeFilters[name] = list = []; 3894 3895 list.push(callback); 3896 }); 3897 }; 3898 3899 self.addAttributeFilter = function(name, callback) { 3900 tinymce.each(tinymce.explode(name), function(name) { 3901 var i; 3902 3903 for (i = 0; i < attributeFilters.length; i++) { 3904 if (attributeFilters[i].name === name) { 3905 attributeFilters[i].callbacks.push(callback); 3906 return; 3907 } 3908 } 3909 3910 attributeFilters.push({name: name, callbacks: [callback]}); 3911 }); 3912 }; 3913 3914 self.parse = function(html, args) { 3915 var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate, 3916 blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement, 3917 endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName; 3918 3919 args = args || {}; 3920 matchedNodes = {}; 3921 matchedAttributes = {}; 3922 blockElements = tinymce.extend(tinymce.makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements()); 3923 nonEmptyElements = schema.getNonEmptyElements(); 3924 children = schema.children; 3925 validate = settings.validate; 3926 rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block; 3927 3928 whiteSpaceElements = schema.getWhiteSpaceElements(); 3929 startWhiteSpaceRegExp = /^[ \t\r\n]+/; 3930 endWhiteSpaceRegExp = /[ \t\r\n]+$/; 3931 allWhiteSpaceRegExp = /[ \t\r\n]+/g; 3932 isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/; 3933 3934 function addRootBlocks() { 3935 var node = rootNode.firstChild, next, rootBlockNode; 3936 3937 while (node) { 3938 next = node.next; 3939 3940 if (node.type == 3 || (node.type == 1 && node.name !== 'p' && !blockElements[node.name] && !node.attr('data-mce-type'))) { 3941 if (!rootBlockNode) { 3942 // Create a new root block element 3943 rootBlockNode = createNode(rootBlockName, 1); 3944 rootNode.insert(rootBlockNode, node); 3945 rootBlockNode.append(node); 3946 } else 3947 rootBlockNode.append(node); 3948 } else { 3949 rootBlockNode = null; 3950 } 3951 3952 node = next; 3953 }; 3954 }; 3955 3956 function createNode(name, type) { 3957 var node = new Node(name, type), list; 3958 3959 if (name in nodeFilters) { 3960 list = matchedNodes[name]; 3961 3962 if (list) 3963 list.push(node); 3964 else 3965 matchedNodes[name] = [node]; 3966 } 3967 3968 return node; 3969 }; 3970 3971 function removeWhitespaceBefore(node) { 3972 var textNode, textVal, sibling; 3973 3974 for (textNode = node.prev; textNode && textNode.type === 3; ) { 3975 textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); 3976 3977 if (textVal.length > 0) { 3978 textNode.value = textVal; 3979 textNode = textNode.prev; 3980 } else { 3981 sibling = textNode.prev; 3982 textNode.remove(); 3983 textNode = sibling; 3984 } 3985 } 3986 }; 3987 3988 function cloneAndExcludeBlocks(input) { 3989 var name, output = {}; 3990 3991 for (name in input) { 3992 if (name !== 'li' && name != 'p') { 3993 output[name] = input[name]; 3994 } 3995 } 3996 3997 return output; 3998 }; 3999 4000 parser = new tinymce.html.SaxParser({ 4001 validate : validate, 4002 4003 // Exclude P and LI from DOM parsing since it's treated better by the DOM parser 4004 self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), 4005 4006 cdata: function(text) { 4007 node.append(createNode('#cdata', 4)).value = text; 4008 }, 4009 4010 text: function(text, raw) { 4011 var textNode; 4012 4013 // Trim all redundant whitespace on non white space elements 4014 if (!isInWhiteSpacePreservedElement) { 4015 text = text.replace(allWhiteSpaceRegExp, ' '); 4016 4017 if (node.lastChild && blockElements[node.lastChild.name]) 4018 text = text.replace(startWhiteSpaceRegExp, ''); 4019 } 4020 4021 // Do we need to create the node 4022 if (text.length !== 0) { 4023 textNode = createNode('#text', 3); 4024 textNode.raw = !!raw; 4025 node.append(textNode).value = text; 4026 } 4027 }, 4028 4029 comment: function(text) { 4030 node.append(createNode('#comment', 8)).value = text; 4031 }, 4032 4033 pi: function(name, text) { 4034 node.append(createNode(name, 7)).value = text; 4035 removeWhitespaceBefore(node); 4036 }, 4037 4038 doctype: function(text) { 4039 var newNode; 4040 4041 newNode = node.append(createNode('#doctype', 10)); 4042 newNode.value = text; 4043 removeWhitespaceBefore(node); 4044 }, 4045 4046 start: function(name, attrs, empty) { 4047 var newNode, attrFiltersLen, elementRule, textNode, attrName, text, sibling, parent; 4048 4049 elementRule = validate ? schema.getElementRule(name) : {}; 4050 if (elementRule) { 4051 newNode = createNode(elementRule.outputName || name, 1); 4052 newNode.attributes = attrs; 4053 newNode.shortEnded = empty; 4054 4055 node.append(newNode); 4056 4057 // Check if node is valid child of the parent node is the child is 4058 // unknown we don't collect it since it's probably a custom element 4059 parent = children[node.name]; 4060 if (parent && children[newNode.name] && !parent[newNode.name]) 4061 invalidChildren.push(newNode); 4062 4063 attrFiltersLen = attributeFilters.length; 4064 while (attrFiltersLen--) { 4065 attrName = attributeFilters[attrFiltersLen].name; 4066 4067 if (attrName in attrs.map) { 4068 list = matchedAttributes[attrName]; 4069 4070 if (list) 4071 list.push(newNode); 4072 else 4073 matchedAttributes[attrName] = [newNode]; 4074 } 4075 } 4076 4077 // Trim whitespace before block 4078 if (blockElements[name]) 4079 removeWhitespaceBefore(newNode); 4080 4081 // Change current node if the element wasn't empty i.e not <br /> or <img /> 4082 if (!empty) 4083 node = newNode; 4084 4085 // Check if we are inside a whitespace preserved element 4086 if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 4087 isInWhiteSpacePreservedElement = true; 4088 } 4089 } 4090 }, 4091 4092 end: function(name) { 4093 var textNode, elementRule, text, sibling, tempNode; 4094 4095 elementRule = validate ? schema.getElementRule(name) : {}; 4096 if (elementRule) { 4097 if (blockElements[name]) { 4098 if (!isInWhiteSpacePreservedElement) { 4099 // Trim whitespace of the first node in a block 4100 textNode = node.firstChild; 4101 if (textNode && textNode.type === 3) { 4102 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 4103 4104 // Any characters left after trim or should we remove it 4105 if (text.length > 0) { 4106 textNode.value = text; 4107 textNode = textNode.next; 4108 } else { 4109 sibling = textNode.next; 4110 textNode.remove(); 4111 textNode = sibling; 4112 } 4113 4114 // Remove any pure whitespace siblings 4115 while (textNode && textNode.type === 3) { 4116 text = textNode.value; 4117 sibling = textNode.next; 4118 4119 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 4120 textNode.remove(); 4121 textNode = sibling; 4122 } 4123 4124 textNode = sibling; 4125 } 4126 } 4127 4128 // Trim whitespace of the last node in a block 4129 textNode = node.lastChild; 4130 if (textNode && textNode.type === 3) { 4131 text = textNode.value.replace(endWhiteSpaceRegExp, ''); 4132 4133 // Any characters left after trim or should we remove it 4134 if (text.length > 0) { 4135 textNode.value = text; 4136 textNode = textNode.prev; 4137 } else { 4138 sibling = textNode.prev; 4139 textNode.remove(); 4140 textNode = sibling; 4141 } 4142 4143 // Remove any pure whitespace siblings 4144 while (textNode && textNode.type === 3) { 4145 text = textNode.value; 4146 sibling = textNode.prev; 4147 4148 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 4149 textNode.remove(); 4150 textNode = sibling; 4151 } 4152 4153 textNode = sibling; 4154 } 4155 } 4156 } 4157 4158 // Trim start white space 4159 textNode = node.prev; 4160 if (textNode && textNode.type === 3) { 4161 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 4162 4163 if (text.length > 0) 4164 textNode.value = text; 4165 else 4166 textNode.remove(); 4167 } 4168 } 4169 4170 // Check if we exited a whitespace preserved element 4171 if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 4172 isInWhiteSpacePreservedElement = false; 4173 } 4174 4175 // Handle empty nodes 4176 if (elementRule.removeEmpty || elementRule.paddEmpty) { 4177 if (node.isEmpty(nonEmptyElements)) { 4178 if (elementRule.paddEmpty) 4179 node.empty().append(new Node('#text', '3')).value = '\u00a0'; 4180 else { 4181 // Leave nodes that have a name like <a name="name"> 4182 if (!node.attributes.map.name && !node.attributes.map.id) { 4183 tempNode = node.parent; 4184 node.empty().remove(); 4185 node = tempNode; 4186 return; 4187 } 4188 } 4189 } 4190 } 4191 4192 node = node.parent; 4193 } 4194 } 4195 }, schema); 4196 4197 rootNode = node = new Node(args.context || settings.root_name, 11); 4198 4199 parser.parse(html); 4200 4201 // Fix invalid children or report invalid children in a contextual parsing 4202 if (validate && invalidChildren.length) { 4203 if (!args.context) 4204 fixInvalidChildren(invalidChildren); 4205 else 4206 args.invalid = true; 4207 } 4208 4209 // Wrap nodes in the root into block elements if the root is body 4210 if (rootBlockName && rootNode.name == 'body') 4211 addRootBlocks(); 4212 4213 // Run filters only when the contents is valid 4214 if (!args.invalid) { 4215 // Run node filters 4216 for (name in matchedNodes) { 4217 list = nodeFilters[name]; 4218 nodes = matchedNodes[name]; 4219 4220 // Remove already removed children 4221 fi = nodes.length; 4222 while (fi--) { 4223 if (!nodes[fi].parent) 4224 nodes.splice(fi, 1); 4225 } 4226 4227 for (i = 0, l = list.length; i < l; i++) 4228 list[i](nodes, name, args); 4229 } 4230 4231 // Run attribute filters 4232 for (i = 0, l = attributeFilters.length; i < l; i++) { 4233 list = attributeFilters[i]; 4234 4235 if (list.name in matchedAttributes) { 4236 nodes = matchedAttributes[list.name]; 4237 4238 // Remove already removed children 4239 fi = nodes.length; 4240 while (fi--) { 4241 if (!nodes[fi].parent) 4242 nodes.splice(fi, 1); 4243 } 4244 4245 for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) 4246 list.callbacks[fi](nodes, list.name, args); 4247 } 4248 } 4249 } 4250 4251 return rootNode; 4252 }; 4253 4254 // Remove <br> at end of block elements Gecko and WebKit injects BR elements to 4255 // make it possible to place the caret inside empty blocks. This logic tries to remove 4256 // these elements and keep br elements that where intended to be there intact 4257 if (settings.remove_trailing_brs) { 4258 self.addNodeFilter('br', function(nodes, name) { 4259 var i, l = nodes.length, node, blockElements = tinymce.extend({}, schema.getBlockElements()), 4260 nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName; 4261 4262 // Remove brs from body element as well 4263 blockElements.body = 1; 4264 4265 // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p> 4266 for (i = 0; i < l; i++) { 4267 node = nodes[i]; 4268 parent = node.parent; 4269 4270 if (blockElements[node.parent.name] && node === parent.lastChild) { 4271 // Loop all nodes to the left of the current node and check for other BR elements 4272 // excluding bookmarks since they are invisible 4273 prev = node.prev; 4274 while (prev) { 4275 prevName = prev.name; 4276 4277 // Ignore bookmarks 4278 if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') { 4279 // Found a non BR element 4280 if (prevName !== "br") 4281 break; 4282 4283 // Found another br it's a <br><br> structure then don't remove anything 4284 if (prevName === 'br') { 4285 node = null; 4286 break; 4287 } 4288 } 4289 4290 prev = prev.prev; 4291 } 4292 4293 if (node) { 4294 node.remove(); 4295 4296 // Is the parent to be considered empty after we removed the BR 4297 if (parent.isEmpty(nonEmptyElements)) { 4298 elementRule = schema.getElementRule(parent.name); 4299 4300 // Remove or padd the element depending on schema rule 4301 if (elementRule) { 4302 if (elementRule.removeEmpty) 4303 parent.remove(); 4304 else if (elementRule.paddEmpty) 4305 parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0'; 4306 } 4307 } 4308 } 4309 } else { 4310 // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p> so they become <p><b><i> </i></b></p> 4311 lastParent = node; 4312 while (parent.firstChild === lastParent && parent.lastChild === lastParent) { 4313 lastParent = parent; 4314 4315 if (blockElements[parent.name]) { 4316 break; 4317 } 4318 4319 parent = parent.parent; 4320 } 4321 4322 if (lastParent === parent) { 4323 textNode = new tinymce.html.Node('#text', 3); 4324 textNode.value = '\u00a0'; 4325 node.replace(textNode); 4326 } 4327 } 4328 } 4329 }); 4330 } 4331 4332 // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. 4333 if (!settings.allow_html_in_named_anchor) { 4334 self.addAttributeFilter('id,name', function(nodes, name) { 4335 var i = nodes.length, sibling, prevSibling, parent, node; 4336 4337 while (i--) { 4338 node = nodes[i]; 4339 if (node.name === 'a' && node.firstChild && !node.attr('href')) { 4340 parent = node.parent; 4341 4342 // Move children after current node 4343 sibling = node.lastChild; 4344 do { 4345 prevSibling = sibling.prev; 4346 parent.insert(sibling, node); 4347 sibling = prevSibling; 4348 } while (sibling); 4349 } 4350 } 4351 }); 4352 } 4353 } 4354 })(tinymce); 4355 4356 tinymce.html.Writer = function(settings) { 4357 var html = [], indent, indentBefore, indentAfter, encode, htmlOutput; 4358 4359 settings = settings || {}; 4360 indent = settings.indent; 4361 indentBefore = tinymce.makeMap(settings.indent_before || ''); 4362 indentAfter = tinymce.makeMap(settings.indent_after || ''); 4363 encode = tinymce.html.Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); 4364 htmlOutput = settings.element_format == "html"; 4365 4366 return { 4367 start: function(name, attrs, empty) { 4368 var i, l, attr, value; 4369 4370 if (indent && indentBefore[name] && html.length > 0) { 4371 value = html[html.length - 1]; 4372 4373 if (value.length > 0 && value !== '\n') 4374 html.push('\n'); 4375 } 4376 4377 html.push('<', name); 4378 4379 if (attrs) { 4380 for (i = 0, l = attrs.length; i < l; i++) { 4381 attr = attrs[i]; 4382 html.push(' ', attr.name, '="', encode(attr.value, true), '"'); 4383 } 4384 } 4385 4386 if (!empty || htmlOutput) 4387 html[html.length] = '>'; 4388 else 4389 html[html.length] = ' />'; 4390 4391 if (empty && indent && indentAfter[name] && html.length > 0) { 4392 value = html[html.length - 1]; 4393 4394 if (value.length > 0 && value !== '\n') 4395 html.push('\n'); 4396 } 4397 }, 4398 4399 end: function(name) { 4400 var value; 4401 4402 /*if (indent && indentBefore[name] && html.length > 0) { 4403 value = html[html.length - 1]; 4404 4405 if (value.length > 0 && value !== '\n') 4406 html.push('\n'); 4407 }*/ 4408 4409 html.push('</', name, '>'); 4410 4411 if (indent && indentAfter[name] && html.length > 0) { 4412 value = html[html.length - 1]; 4413 4414 if (value.length > 0 && value !== '\n') 4415 html.push('\n'); 4416 } 4417 }, 4418 4419 text: function(text, raw) { 4420 if (text.length > 0) 4421 html[html.length] = raw ? text : encode(text); 4422 }, 4423 4424 cdata: function(text) { 4425 html.push('<![CDATA[', text, ']]>'); 4426 }, 4427 4428 comment: function(text) { 4429 html.push('<!--', text, '-->'); 4430 }, 4431 4432 pi: function(name, text) { 4433 if (text) 4434 html.push('<?', name, ' ', text, '?>'); 4435 else 4436 html.push('<?', name, '?>'); 4437 4438 if (indent) 4439 html.push('\n'); 4440 }, 4441 4442 doctype: function(text) { 4443 html.push('<!DOCTYPE', text, '>', indent ? '\n' : ''); 4444 }, 4445 4446 reset: function() { 4447 html.length = 0; 4448 }, 4449 4450 getContent: function() { 4451 return html.join('').replace(/\n$/, ''); 4452 } 4453 }; 4454 }; 4455 4456 (function(tinymce) { 4457 tinymce.html.Serializer = function(settings, schema) { 4458 var self = this, writer = new tinymce.html.Writer(settings); 4459 4460 settings = settings || {}; 4461 settings.validate = "validate" in settings ? settings.validate : true; 4462 4463 self.schema = schema = schema || new tinymce.html.Schema(); 4464 self.writer = writer; 4465 4466 self.serialize = function(node) { 4467 var handlers, validate; 4468 4469 validate = settings.validate; 4470 4471 handlers = { 4472 // #text 4473 3: function(node, raw) { 4474 writer.text(node.value, node.raw); 4475 }, 4476 4477 // #comment 4478 8: function(node) { 4479 writer.comment(node.value); 4480 }, 4481 4482 // Processing instruction 4483 7: function(node) { 4484 writer.pi(node.name, node.value); 4485 }, 4486 4487 // Doctype 4488 10: function(node) { 4489 writer.doctype(node.value); 4490 }, 4491 4492 // CDATA 4493 4: function(node) { 4494 writer.cdata(node.value); 4495 }, 4496 4497 // Document fragment 4498 11: function(node) { 4499 if ((node = node.firstChild)) { 4500 do { 4501 walk(node); 4502 } while (node = node.next); 4503 } 4504 } 4505 }; 4506 4507 writer.reset(); 4508 4509 function walk(node) { 4510 var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; 4511 4512 if (!handler) { 4513 name = node.name; 4514 isEmpty = node.shortEnded; 4515 attrs = node.attributes; 4516 4517 // Sort attributes 4518 if (validate && attrs && attrs.length > 1) { 4519 sortedAttrs = []; 4520 sortedAttrs.map = {}; 4521 4522 elementRule = schema.getElementRule(node.name); 4523 for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { 4524 attrName = elementRule.attributesOrder[i]; 4525 4526 if (attrName in attrs.map) { 4527 attrValue = attrs.map[attrName]; 4528 sortedAttrs.map[attrName] = attrValue; 4529 sortedAttrs.push({name: attrName, value: attrValue}); 4530 } 4531 } 4532 4533 for (i = 0, l = attrs.length; i < l; i++) { 4534 attrName = attrs[i].name; 4535 4536 if (!(attrName in sortedAttrs.map)) { 4537 attrValue = attrs.map[attrName]; 4538 sortedAttrs.map[attrName] = attrValue; 4539 sortedAttrs.push({name: attrName, value: attrValue}); 4540 } 4541 } 4542 4543 attrs = sortedAttrs; 4544 } 4545 4546 writer.start(node.name, attrs, isEmpty); 4547 4548 if (!isEmpty) { 4549 if ((node = node.firstChild)) { 4550 do { 4551 walk(node); 4552 } while (node = node.next); 4553 } 4554 4555 writer.end(name); 4556 } 4557 } else 4558 handler(node); 4559 } 4560 4561 // Serialize element and treat all non elements as fragments 4562 if (node.type == 1 && !settings.inner) 4563 walk(node); 4564 else 4565 handlers[11](node); 4566 4567 return writer.getContent(); 4568 }; 4569 } 4570 })(tinymce); 4571 4572 // JSLint defined globals 4573 /*global tinymce:false, window:false */ 4574 4575 tinymce.dom = {}; 4576 4577 (function(namespace, expando) { 4578 var w3cEventModel = !!document.addEventListener; 4579 4580 function addEvent(target, name, callback, capture) { 4581 if (target.addEventListener) { 4582 target.addEventListener(name, callback, capture || false); 4583 } else if (target.attachEvent) { 4584 target.attachEvent('on' + name, callback); 4585 } 4586 } 4587 4588 function removeEvent(target, name, callback, capture) { 4589 if (target.removeEventListener) { 4590 target.removeEventListener(name, callback, capture || false); 4591 } else if (target.detachEvent) { 4592 target.detachEvent('on' + name, callback); 4593 } 4594 } 4595 4596 function fix(original_event, data) { 4597 var name, event = data || {}; 4598 4599 // Dummy function that gets replaced on the delegation state functions 4600 function returnFalse() { 4601 return false; 4602 } 4603 4604 // Dummy function that gets replaced on the delegation state functions 4605 function returnTrue() { 4606 return true; 4607 } 4608 4609 // Copy all properties from the original event 4610 for (name in original_event) { 4611 // layerX/layerY is deprecated in Chrome and produces a warning 4612 if (name !== "layerX" && name !== "layerY") { 4613 event[name] = original_event[name]; 4614 } 4615 } 4616 4617 // Normalize target IE uses srcElement 4618 if (!event.target) { 4619 event.target = event.srcElement || document; 4620 } 4621 4622 // Add preventDefault method 4623 event.preventDefault = function() { 4624 event.isDefaultPrevented = returnTrue; 4625 4626 // Execute preventDefault on the original event object 4627 if (original_event) { 4628 if (original_event.preventDefault) { 4629 original_event.preventDefault(); 4630 } else { 4631 original_event.returnValue = false; // IE 4632 } 4633 } 4634 }; 4635 4636 // Add stopPropagation 4637 event.stopPropagation = function() { 4638 event.isPropagationStopped = returnTrue; 4639 4640 // Execute stopPropagation on the original event object 4641 if (original_event) { 4642 if (original_event.stopPropagation) { 4643 original_event.stopPropagation(); 4644 } else { 4645 original_event.cancelBubble = true; // IE 4646 } 4647 } 4648 }; 4649 4650 // Add stopImmediatePropagation 4651 event.stopImmediatePropagation = function() { 4652 event.isImmediatePropagationStopped = returnTrue; 4653 event.stopPropagation(); 4654 }; 4655 4656 // Add event delegation states 4657 if (!event.isDefaultPrevented) { 4658 event.isDefaultPrevented = returnFalse; 4659 event.isPropagationStopped = returnFalse; 4660 event.isImmediatePropagationStopped = returnFalse; 4661 } 4662 4663 return event; 4664 } 4665 4666 function bindOnReady(win, callback, event_utils) { 4667 var doc = win.document, event = {type: 'ready'}; 4668 4669 // Gets called when the DOM is ready 4670 function readyHandler() { 4671 if (!event_utils.domLoaded) { 4672 event_utils.domLoaded = true; 4673 callback(event); 4674 } 4675 } 4676 4677 // Use W3C method 4678 if (w3cEventModel) { 4679 addEvent(win, 'DOMContentLoaded', readyHandler); 4680 } else { 4681 // Use IE method 4682 addEvent(doc, "readystatechange", function() { 4683 if (doc.readyState === "complete") { 4684 removeEvent(doc, "readystatechange", arguments.callee); 4685 readyHandler(); 4686 } 4687 }); 4688 4689 // Wait until we can scroll, when we can the DOM is initialized 4690 if (doc.documentElement.doScroll && win === win.top) { 4691 (function() { 4692 try { 4693 // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author. 4694 // http://javascript.nwbox.com/IEContentLoaded/ 4695 doc.documentElement.doScroll("left"); 4696 } catch (ex) { 4697 setTimeout(arguments.callee, 0); 4698 return; 4699 } 4700 4701 readyHandler(); 4702 })(); 4703 } 4704 } 4705 4706 // Fallback if any of the above methods should fail for some odd reason 4707 addEvent(win, 'load', readyHandler); 4708 } 4709 4710 function EventUtils(proxy) { 4711 var self = this, events = {}, count, isFocusBlurBound, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave; 4712 4713 hasMouseEnterLeave = "onmouseenter" in document.documentElement; 4714 hasFocusIn = "onfocusin" in document.documentElement; 4715 mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'}; 4716 count = 1; 4717 4718 // State if the DOMContentLoaded was executed or not 4719 self.domLoaded = false; 4720 self.events = events; 4721 4722 function executeHandlers(evt, id) { 4723 var callbackList, i, l, callback; 4724 4725 callbackList = events[id][evt.type]; 4726 if (callbackList) { 4727 for (i = 0, l = callbackList.length; i < l; i++) { 4728 callback = callbackList[i]; 4729 4730 // Check if callback exists might be removed if a unbind is called inside the callback 4731 if (callback && callback.func.call(callback.scope, evt) === false) { 4732 evt.preventDefault(); 4733 } 4734 4735 // Should we stop propagation to immediate listeners 4736 if (evt.isImmediatePropagationStopped()) { 4737 return; 4738 } 4739 } 4740 } 4741 } 4742 4743 self.bind = function(target, names, callback, scope) { 4744 var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window; 4745 4746 // Native event handler function patches the event and executes the callbacks for the expando 4747 function defaultNativeHandler(evt) { 4748 executeHandlers(fix(evt || win.event), id); 4749 } 4750 4751 // Don't bind to text nodes or comments 4752 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4753 return; 4754 } 4755 4756 // Create or get events id for the target 4757 if (!target[expando]) { 4758 id = count++; 4759 target[expando] = id; 4760 events[id] = {}; 4761 } else { 4762 id = target[expando]; 4763 4764 if (!events[id]) { 4765 events[id] = {}; 4766 } 4767 } 4768 4769 // Setup the specified scope or use the target as a default 4770 scope = scope || target; 4771 4772 // Split names and bind each event, enables you to bind multiple events with one call 4773 names = names.split(' '); 4774 i = names.length; 4775 while (i--) { 4776 name = names[i]; 4777 nativeHandler = defaultNativeHandler; 4778 fakeName = capture = false; 4779 4780 // Use ready instead of DOMContentLoaded 4781 if (name === "DOMContentLoaded") { 4782 name = "ready"; 4783 } 4784 4785 // DOM is already ready 4786 if ((self.domLoaded || target.readyState == 'complete') && name === "ready") { 4787 self.domLoaded = true; 4788 callback.call(scope, fix({type: name})); 4789 continue; 4790 } 4791 4792 // Handle mouseenter/mouseleaver 4793 if (!hasMouseEnterLeave) { 4794 fakeName = mouseEnterLeave[name]; 4795 4796 if (fakeName) { 4797 nativeHandler = function(evt) { 4798 var current, related; 4799 4800 current = evt.currentTarget; 4801 related = evt.relatedTarget; 4802 4803 // Check if related is inside the current target if it's not then the event should be ignored since it's a mouseover/mouseout inside the element 4804 if (related && current.contains) { 4805 // Use contains for performance 4806 related = current.contains(related); 4807 } else { 4808 while (related && related !== current) { 4809 related = related.parentNode; 4810 } 4811 } 4812 4813 // Fire fake event 4814 if (!related) { 4815 evt = fix(evt || win.event); 4816 evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter'; 4817 evt.target = current; 4818 executeHandlers(evt, id); 4819 } 4820 }; 4821 } 4822 } 4823 4824 // Fake bubbeling of focusin/focusout 4825 if (!hasFocusIn && (name === "focusin" || name === "focusout")) { 4826 capture = true; 4827 fakeName = name === "focusin" ? "focus" : "blur"; 4828 nativeHandler = function(evt) { 4829 evt = fix(evt || win.event); 4830 evt.type = evt.type === 'focus' ? 'focusin' : 'focusout'; 4831 executeHandlers(evt, id); 4832 }; 4833 } 4834 4835 // Setup callback list and bind native event 4836 callbackList = events[id][name]; 4837 if (!callbackList) { 4838 events[id][name] = callbackList = [{func: callback, scope: scope}]; 4839 callbackList.fakeName = fakeName; 4840 callbackList.capture = capture; 4841 4842 // Add the nativeHandler to the callback list so that we can later unbind it 4843 callbackList.nativeHandler = nativeHandler; 4844 if (!w3cEventModel) { 4845 callbackList.proxyHandler = proxy(id); 4846 } 4847 4848 // Check if the target has native events support 4849 if (name === "ready") { 4850 bindOnReady(target, nativeHandler, self); 4851 } else { 4852 addEvent(target, fakeName || name, w3cEventModel ? nativeHandler : callbackList.proxyHandler, capture); 4853 } 4854 } else { 4855 // If it already has an native handler then just push the callback 4856 callbackList.push({func: callback, scope: scope}); 4857 } 4858 } 4859 4860 target = callbackList = 0; // Clean memory for IE 4861 4862 return callback; 4863 }; 4864 4865 self.unbind = function(target, names, callback) { 4866 var id, callbackList, i, ci, name, eventMap; 4867 4868 // Don't bind to text nodes or comments 4869 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4870 return self; 4871 } 4872 4873 // Unbind event or events if the target has the expando 4874 id = target[expando]; 4875 if (id) { 4876 eventMap = events[id]; 4877 4878 // Specific callback 4879 if (names) { 4880 names = names.split(' '); 4881 i = names.length; 4882 while (i--) { 4883 name = names[i]; 4884 callbackList = eventMap[name]; 4885 4886 // Unbind the event if it exists in the map 4887 if (callbackList) { 4888 // Remove specified callback 4889 if (callback) { 4890 ci = callbackList.length; 4891 while (ci--) { 4892 if (callbackList[ci].func === callback) { 4893 callbackList.splice(ci, 1); 4894 } 4895 } 4896 } 4897 4898 // Remove all callbacks if there isn't a specified callback or there is no callbacks left 4899 if (!callback || callbackList.length === 0) { 4900 delete eventMap[name]; 4901 removeEvent(target, callbackList.fakeName || name, w3cEventModel ? callbackList.nativeHandler : callbackList.proxyHandler, callbackList.capture); 4902 } 4903 } 4904 } 4905 } else { 4906 // All events for a specific element 4907 for (name in eventMap) { 4908 callbackList = eventMap[name]; 4909 removeEvent(target, callbackList.fakeName || name, w3cEventModel ? callbackList.nativeHandler : callbackList.proxyHandler, callbackList.capture); 4910 } 4911 4912 eventMap = {}; 4913 } 4914 4915 // Check if object is empty, if it isn't then we won't remove the expando map 4916 for (name in eventMap) { 4917 return self; 4918 } 4919 4920 // Delete event object 4921 delete events[id]; 4922 4923 // Remove expando from target 4924 try { 4925 // IE will fail here since it can't delete properties from window 4926 delete target[expando]; 4927 } catch (ex) { 4928 // IE will set it to null 4929 target[expando] = null; 4930 } 4931 } 4932 4933 return self; 4934 }; 4935 4936 self.fire = function(target, name, args) { 4937 var id, event; 4938 4939 // Don't bind to text nodes or comments 4940 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4941 return self; 4942 } 4943 4944 // Build event object by patching the args 4945 event = fix(null, args); 4946 event.type = name; 4947 4948 do { 4949 // Found an expando that means there is listeners to execute 4950 id = target[expando]; 4951 if (id) { 4952 executeHandlers(event, id); 4953 } 4954 4955 // Walk up the DOM 4956 target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow; 4957 } while (target && !event.isPropagationStopped()); 4958 4959 return self; 4960 }; 4961 4962 self.clean = function(target) { 4963 var i, children, unbind = self.unbind; 4964 4965 // Don't bind to text nodes or comments 4966 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4967 return self; 4968 } 4969 4970 // Unbind any element on the specificed target 4971 if (target[expando]) { 4972 unbind(target); 4973 } 4974 4975 // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children 4976 if (!target.getElementsByTagName) { 4977 target = target.document; 4978 } 4979 4980 // Remove events from each child element 4981 if (target && target.getElementsByTagName) { 4982 unbind(target); 4983 4984 children = target.getElementsByTagName('*'); 4985 i = children.length; 4986 while (i--) { 4987 target = children[i]; 4988 4989 if (target[expando]) { 4990 unbind(target); 4991 } 4992 } 4993 } 4994 4995 return self; 4996 }; 4997 4998 self.callNativeHandler = function(id, evt) { 4999 if (events) { 5000 events[id][evt.type].nativeHandler(evt); 5001 } 5002 }; 5003 5004 self.destory = function() { 5005 events = {}; 5006 }; 5007 5008 // Legacy function calls 5009 5010 self.add = function(target, events, func, scope) { 5011 // Old API supported direct ID assignment 5012 if (typeof(target) === "string") { 5013 target = document.getElementById(target); 5014 } 5015 5016 // Old API supported multiple targets 5017 if (target && target instanceof Array) { 5018 var i = target.length; 5019 5020 while (i--) { 5021 self.add(target[i], events, func, scope); 5022 } 5023 5024 return; 5025 } 5026 5027 // Old API called ready init 5028 if (events === "init") { 5029 events = "ready"; 5030 } 5031 5032 return self.bind(target, events instanceof Array ? events.join(' ') : events, func, scope); 5033 }; 5034 5035 self.remove = function(target, events, func, scope) { 5036 if (!target) { 5037 return self; 5038 } 5039 5040 // Old API supported direct ID assignment 5041 if (typeof(target) === "string") { 5042 target = document.getElementById(target); 5043 } 5044 5045 // Old API supported multiple targets 5046 if (target instanceof Array) { 5047 var i = target.length; 5048 5049 while (i--) { 5050 self.remove(target[i], events, func, scope); 5051 } 5052 5053 return self; 5054 } 5055 5056 return self.unbind(target, events instanceof Array ? events.join(' ') : events, func); 5057 }; 5058 5059 self.clear = function(target) { 5060 // Old API supported direct ID assignment 5061 if (typeof(target) === "string") { 5062 target = document.getElementById(target); 5063 } 5064 5065 return self.clean(target); 5066 }; 5067 5068 self.cancel = function(e) { 5069 if (e) { 5070 self.prevent(e); 5071 self.stop(e); 5072 } 5073 5074 return false; 5075 }; 5076 5077 self.prevent = function(e) { 5078 if (!e.preventDefault) { 5079 e = fix(e); 5080 } 5081 5082 e.preventDefault(); 5083 5084 return false; 5085 }; 5086 5087 self.stop = function(e) { 5088 if (!e.stopPropagation) { 5089 e = fix(e); 5090 } 5091 5092 e.stopPropagation(); 5093 5094 return false; 5095 }; 5096 } 5097 5098 namespace.EventUtils = EventUtils; 5099 5100 namespace.Event = new EventUtils(function(id) { 5101 return function(evt) { 5102 tinymce.dom.Event.callNativeHandler(id, evt); 5103 }; 5104 }); 5105 5106 // Bind ready event when tinymce script is loaded 5107 namespace.Event.bind(window, 'ready', function() {}); 5108 5109 namespace = 0; 5110 })(tinymce.dom, 'data-mce-expando'); // Namespace and expando 5111 5112 tinymce.dom.TreeWalker = function(start_node, root_node) { 5113 var node = start_node; 5114 5115 function findSibling(node, start_name, sibling_name, shallow) { 5116 var sibling, parent; 5117 5118 if (node) { 5119 // Walk into nodes if it has a start 5120 if (!shallow && node[start_name]) 5121 return node[start_name]; 5122 5123 // Return the sibling if it has one 5124 if (node != root_node) { 5125 sibling = node[sibling_name]; 5126 if (sibling) 5127 return sibling; 5128 5129 // Walk up the parents to look for siblings 5130 for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) { 5131 sibling = parent[sibling_name]; 5132 if (sibling) 5133 return sibling; 5134 } 5135 } 5136 } 5137 }; 5138 5139 this.current = function() { 5140 return node; 5141 }; 5142 5143 this.next = function(shallow) { 5144 return (node = findSibling(node, 'firstChild', 'nextSibling', shallow)); 5145 }; 5146 5147 this.prev = function(shallow) { 5148 return (node = findSibling(node, 'lastChild', 'previousSibling', shallow)); 5149 }; 5150 }; 5151 5152 (function(tinymce) { 5153 // Shorten names 5154 var each = tinymce.each, 5155 is = tinymce.is, 5156 isWebKit = tinymce.isWebKit, 5157 isIE = tinymce.isIE, 5158 Entities = tinymce.html.Entities, 5159 simpleSelectorRe = /^([a-z0-9],?)+$/i, 5160 whiteSpaceRegExp = /^[ \t\r\n]*$/; 5161 5162 tinymce.create('tinymce.dom.DOMUtils', { 5163 doc : null, 5164 root : null, 5165 files : null, 5166 pixelStyles : /^(top|left|bottom|right|width|height|borderWidth)$/, 5167 props : { 5168 "for" : "htmlFor", 5169 "class" : "className", 5170 className : "className", 5171 checked : "checked", 5172 disabled : "disabled", 5173 maxlength : "maxLength", 5174 readonly : "readOnly", 5175 selected : "selected", 5176 value : "value", 5177 id : "id", 5178 name : "name", 5179 type : "type" 5180 }, 5181 5182 DOMUtils : function(d, s) { 5183 var t = this, globalStyle, name, blockElementsMap; 5184 5185 t.doc = d; 5186 t.win = window; 5187 t.files = {}; 5188 t.cssFlicker = false; 5189 t.counter = 0; 5190 t.stdMode = !tinymce.isIE || d.documentMode >= 8; 5191 t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat" || t.stdMode; 5192 t.hasOuterHTML = "outerHTML" in d.createElement("a"); 5193 5194 t.settings = s = tinymce.extend({ 5195 keep_values : false, 5196 hex_colors : 1 5197 }, s); 5198 5199 t.schema = s.schema; 5200 t.styles = new tinymce.html.Styles({ 5201 url_converter : s.url_converter, 5202 url_converter_scope : s.url_converter_scope 5203 }, s.schema); 5204 5205 // Fix IE6SP2 flicker and check it failed for pre SP2 5206 if (tinymce.isIE6) { 5207 try { 5208 d.execCommand('BackgroundImageCache', false, true); 5209 } catch (e) { 5210 t.cssFlicker = true; 5211 } 5212 } 5213 5214 t.fixDoc(d); 5215 t.events = s.ownEvents ? new tinymce.dom.EventUtils(s.proxy) : tinymce.dom.Event; 5216 tinymce.addUnload(t.destroy, t); 5217 blockElementsMap = s.schema ? s.schema.getBlockElements() : {}; 5218 5219 t.isBlock = function(node) { 5220 // This function is called in module pattern style since it might be executed with the wrong this scope 5221 var type = node.nodeType; 5222 5223 // If it's a node then check the type and use the nodeName 5224 if (type) 5225 return !!(type === 1 && blockElementsMap[node.nodeName]); 5226 5227 return !!blockElementsMap[node]; 5228 }; 5229 }, 5230 5231 fixDoc: function(doc) { 5232 var settings = this.settings, name; 5233 5234 if (isIE && settings.schema) { 5235 // Add missing HTML 4/5 elements to IE 5236 ('abbr article aside audio canvas ' + 5237 'details figcaption figure footer ' + 5238 'header hgroup mark menu meter nav ' + 5239 'output progress section summary ' + 5240 'time video').replace(/\w+/g, function(name) { 5241 doc.createElement(name); 5242 }); 5243 5244 // Create all custom elements 5245 for (name in settings.schema.getCustomElements()) { 5246 doc.createElement(name); 5247 } 5248 } 5249 }, 5250 5251 clone: function(node, deep) { 5252 var self = this, clone, doc; 5253 5254 // TODO: Add feature detection here in the future 5255 if (!isIE || node.nodeType !== 1 || deep) { 5256 return node.cloneNode(deep); 5257 } 5258 5259 doc = self.doc; 5260 5261 // Make a HTML5 safe shallow copy 5262 if (!deep) { 5263 clone = doc.createElement(node.nodeName); 5264 5265 // Copy attribs 5266 each(self.getAttribs(node), function(attr) { 5267 self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName)); 5268 }); 5269 5270 return clone; 5271 } 5272 /* 5273 // Setup HTML5 patched document fragment 5274 if (!self.frag) { 5275 self.frag = doc.createDocumentFragment(); 5276 self.fixDoc(self.frag); 5277 } 5278 5279 // Make a deep copy by adding it to the document fragment then removing it this removed the :section 5280 clone = doc.createElement('div'); 5281 self.frag.appendChild(clone); 5282 clone.innerHTML = node.outerHTML; 5283 self.frag.removeChild(clone); 5284 */ 5285 return clone.firstChild; 5286 }, 5287 5288 getRoot : function() { 5289 var t = this, s = t.settings; 5290 5291 return (s && t.get(s.root_element)) || t.doc.body; 5292 }, 5293 5294 getViewPort : function(w) { 5295 var d, b; 5296 5297 w = !w ? this.win : w; 5298 d = w.document; 5299 b = this.boxModel ? d.documentElement : d.body; 5300 5301 // Returns viewport size excluding scrollbars 5302 return { 5303 x : w.pageXOffset || b.scrollLeft, 5304 y : w.pageYOffset || b.scrollTop, 5305 w : w.innerWidth || b.clientWidth, 5306 h : w.innerHeight || b.clientHeight 5307 }; 5308 }, 5309 5310 getRect : function(e) { 5311 var p, t = this, sr; 5312 5313 e = t.get(e); 5314 p = t.getPos(e); 5315 sr = t.getSize(e); 5316 5317 return { 5318 x : p.x, 5319 y : p.y, 5320 w : sr.w, 5321 h : sr.h 5322 }; 5323 }, 5324 5325 getSize : function(e) { 5326 var t = this, w, h; 5327 5328 e = t.get(e); 5329 w = t.getStyle(e, 'width'); 5330 h = t.getStyle(e, 'height'); 5331 5332 // Non pixel value, then force offset/clientWidth 5333 if (w.indexOf('px') === -1) 5334 w = 0; 5335 5336 // Non pixel value, then force offset/clientWidth 5337 if (h.indexOf('px') === -1) 5338 h = 0; 5339 5340 return { 5341 w : parseInt(w, 10) || e.offsetWidth || e.clientWidth, 5342 h : parseInt(h, 10) || e.offsetHeight || e.clientHeight 5343 }; 5344 }, 5345 5346 getParent : function(n, f, r) { 5347 return this.getParents(n, f, r, false); 5348 }, 5349 5350 getParents : function(n, f, r, c) { 5351 var t = this, na, se = t.settings, o = []; 5352 5353 n = t.get(n); 5354 c = c === undefined; 5355 5356 if (se.strict_root) 5357 r = r || t.getRoot(); 5358 5359 // Wrap node name as func 5360 if (is(f, 'string')) { 5361 na = f; 5362 5363 if (f === '*') { 5364 f = function(n) {return n.nodeType == 1;}; 5365 } else { 5366 f = function(n) { 5367 return t.is(n, na); 5368 }; 5369 } 5370 } 5371 5372 while (n) { 5373 if (n == r || !n.nodeType || n.nodeType === 9) 5374 break; 5375 5376 if (!f || f(n)) { 5377 if (c) 5378 o.push(n); 5379 else 5380 return n; 5381 } 5382 5383 n = n.parentNode; 5384 } 5385 5386 return c ? o : null; 5387 }, 5388 5389 get : function(e) { 5390 var n; 5391 5392 if (e && this.doc && typeof(e) == 'string') { 5393 n = e; 5394 e = this.doc.getElementById(e); 5395 5396 // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick 5397 if (e && e.id !== n) 5398 return this.doc.getElementsByName(n)[1]; 5399 } 5400 5401 return e; 5402 }, 5403 5404 getNext : function(node, selector) { 5405 return this._findSib(node, selector, 'nextSibling'); 5406 }, 5407 5408 getPrev : function(node, selector) { 5409 return this._findSib(node, selector, 'previousSibling'); 5410 }, 5411 5412 5413 add : function(p, n, a, h, c) { 5414 var t = this; 5415 5416 return this.run(p, function(p) { 5417 var e, k; 5418 5419 e = is(n, 'string') ? t.doc.createElement(n) : n; 5420 t.setAttribs(e, a); 5421 5422 if (h) { 5423 if (h.nodeType) 5424 e.appendChild(h); 5425 else 5426 t.setHTML(e, h); 5427 } 5428 5429 return !c ? p.appendChild(e) : e; 5430 }); 5431 }, 5432 5433 create : function(n, a, h) { 5434 return this.add(this.doc.createElement(n), n, a, h, 1); 5435 }, 5436 5437 createHTML : function(n, a, h) { 5438 var o = '', t = this, k; 5439 5440 o += '<' + n; 5441 5442 for (k in a) { 5443 if (a.hasOwnProperty(k)) 5444 o += ' ' + k + '="' + t.encode(a[k]) + '"'; 5445 } 5446 5447 // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime 5448 if (typeof(h) != "undefined") 5449 return o + '>' + h + '</' + n + '>'; 5450 5451 return o + ' />'; 5452 }, 5453 5454 remove : function(node, keep_children) { 5455 return this.run(node, function(node) { 5456 var child, parent = node.parentNode; 5457 5458 if (!parent) 5459 return null; 5460 5461 if (keep_children) { 5462 while (child = node.firstChild) { 5463 // IE 8 will crash if you don't remove completely empty text nodes 5464 if (!tinymce.isIE || child.nodeType !== 3 || child.nodeValue) 5465 parent.insertBefore(child, node); 5466 else 5467 node.removeChild(child); 5468 } 5469 } 5470 5471 return parent.removeChild(node); 5472 }); 5473 }, 5474 5475 setStyle : function(n, na, v) { 5476 var t = this; 5477 5478 return t.run(n, function(e) { 5479 var s, i; 5480 5481 s = e.style; 5482 5483 // Camelcase it, if needed 5484 na = na.replace(/-(\D)/g, function(a, b){ 5485 return b.toUpperCase(); 5486 }); 5487 5488 // Default px suffix on these 5489 if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v))) 5490 v += 'px'; 5491 5492 switch (na) { 5493 case 'opacity': 5494 // IE specific opacity 5495 if (isIE) { 5496 s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")"; 5497 5498 if (!n.currentStyle || !n.currentStyle.hasLayout) 5499 s.display = 'inline-block'; 5500 } 5501 5502 // Fix for older browsers 5503 s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || ''; 5504 break; 5505 5506 case 'float': 5507 isIE ? s.styleFloat = v : s.cssFloat = v; 5508 break; 5509 5510 default: 5511 s[na] = v || ''; 5512 } 5513 5514 // Force update of the style data 5515 if (t.settings.update_styles) 5516 t.setAttrib(e, 'data-mce-style'); 5517 }); 5518 }, 5519 5520 getStyle : function(n, na, c) { 5521 n = this.get(n); 5522 5523 if (!n) 5524 return; 5525 5526 // Gecko 5527 if (this.doc.defaultView && c) { 5528 // Remove camelcase 5529 na = na.replace(/[A-Z]/g, function(a){ 5530 return '-' + a; 5531 }); 5532 5533 try { 5534 return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na); 5535 } catch (ex) { 5536 // Old safari might fail 5537 return null; 5538 } 5539 } 5540 5541 // Camelcase it, if needed 5542 na = na.replace(/-(\D)/g, function(a, b){ 5543 return b.toUpperCase(); 5544 }); 5545 5546 if (na == 'float') 5547 na = isIE ? 'styleFloat' : 'cssFloat'; 5548 5549 // IE & Opera 5550 if (n.currentStyle && c) 5551 return n.currentStyle[na]; 5552 5553 return n.style ? n.style[na] : undefined; 5554 }, 5555 5556 setStyles : function(e, o) { 5557 var t = this, s = t.settings, ol; 5558 5559 ol = s.update_styles; 5560 s.update_styles = 0; 5561 5562 each(o, function(v, n) { 5563 t.setStyle(e, n, v); 5564 }); 5565 5566 // Update style info 5567 s.update_styles = ol; 5568 if (s.update_styles) 5569 t.setAttrib(e, s.cssText); 5570 }, 5571 5572 removeAllAttribs: function(e) { 5573 return this.run(e, function(e) { 5574 var i, attrs = e.attributes; 5575 for (i = attrs.length - 1; i >= 0; i--) { 5576 e.removeAttributeNode(attrs.item(i)); 5577 } 5578 }); 5579 }, 5580 5581 setAttrib : function(e, n, v) { 5582 var t = this; 5583 5584 // Whats the point 5585 if (!e || !n) 5586 return; 5587 5588 // Strict XML mode 5589 if (t.settings.strict) 5590 n = n.toLowerCase(); 5591 5592 return this.run(e, function(e) { 5593 var s = t.settings; 5594 var originalValue = e.getAttribute(n); 5595 if (v !== null) { 5596 switch (n) { 5597 case "style": 5598 if (!is(v, 'string')) { 5599 each(v, function(v, n) { 5600 t.setStyle(e, n, v); 5601 }); 5602 5603 return; 5604 } 5605 5606 // No mce_style for elements with these since they might get resized by the user 5607 if (s.keep_values) { 5608 if (v && !t._isRes(v)) 5609 e.setAttribute('data-mce-style', v, 2); 5610 else 5611 e.removeAttribute('data-mce-style', 2); 5612 } 5613 5614 e.style.cssText = v; 5615 break; 5616 5617 case "class": 5618 e.className = v || ''; // Fix IE null bug 5619 break; 5620 5621 case "src": 5622 case "href": 5623 if (s.keep_values) { 5624 if (s.url_converter) 5625 v = s.url_converter.call(s.url_converter_scope || t, v, n, e); 5626 5627 t.setAttrib(e, 'data-mce-' + n, v, 2); 5628 } 5629 5630 break; 5631 5632 case "shape": 5633 e.setAttribute('data-mce-style', v); 5634 break; 5635 } 5636 } 5637 if (is(v) && v !== null && v.length !== 0) 5638 e.setAttribute(n, '' + v, 2); 5639 else 5640 e.removeAttribute(n, 2); 5641 5642 // fire onChangeAttrib event for attributes that have changed 5643 if (tinyMCE.activeEditor && originalValue != v) { 5644 var ed = tinyMCE.activeEditor; 5645 ed.onSetAttrib.dispatch(ed, e, n, v); 5646 } 5647 }); 5648 }, 5649 5650 setAttribs : function(e, o) { 5651 var t = this; 5652 5653 return this.run(e, function(e) { 5654 each(o, function(v, n) { 5655 t.setAttrib(e, n, v); 5656 }); 5657 }); 5658 }, 5659 5660 getAttrib : function(e, n, dv) { 5661 var v, t = this, undef; 5662 5663 e = t.get(e); 5664 5665 if (!e || e.nodeType !== 1) 5666 return dv === undef ? false : dv; 5667 5668 if (!is(dv)) 5669 dv = ''; 5670 5671 // Try the mce variant for these 5672 if (/^(src|href|style|coords|shape)$/.test(n)) { 5673 v = e.getAttribute("data-mce-" + n); 5674 5675 if (v) 5676 return v; 5677 } 5678 5679 if (isIE && t.props[n]) { 5680 v = e[t.props[n]]; 5681 v = v && v.nodeValue ? v.nodeValue : v; 5682 } 5683 5684 if (!v) 5685 v = e.getAttribute(n, 2); 5686 5687 // Check boolean attribs 5688 if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(n)) { 5689 if (e[t.props[n]] === true && v === '') 5690 return n; 5691 5692 return v ? n : ''; 5693 } 5694 5695 // Inner input elements will override attributes on form elements 5696 if (e.nodeName === "FORM" && e.getAttributeNode(n)) 5697 return e.getAttributeNode(n).nodeValue; 5698 5699 if (n === 'style') { 5700 v = v || e.style.cssText; 5701 5702 if (v) { 5703 v = t.serializeStyle(t.parseStyle(v), e.nodeName); 5704 5705 if (t.settings.keep_values && !t._isRes(v)) 5706 e.setAttribute('data-mce-style', v); 5707 } 5708 } 5709 5710 // Remove Apple and WebKit stuff 5711 if (isWebKit && n === "class" && v) 5712 v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, ''); 5713 5714 // Handle IE issues 5715 if (isIE) { 5716 switch (n) { 5717 case 'rowspan': 5718 case 'colspan': 5719 // IE returns 1 as default value 5720 if (v === 1) 5721 v = ''; 5722 5723 break; 5724 5725 case 'size': 5726 // IE returns +0 as default value for size 5727 if (v === '+0' || v === 20 || v === 0) 5728 v = ''; 5729 5730 break; 5731 5732 case 'width': 5733 case 'height': 5734 case 'vspace': 5735 case 'checked': 5736 case 'disabled': 5737 case 'readonly': 5738 if (v === 0) 5739 v = ''; 5740 5741 break; 5742 5743 case 'hspace': 5744 // IE returns -1 as default value 5745 if (v === -1) 5746 v = ''; 5747 5748 break; 5749 5750 case 'maxlength': 5751 case 'tabindex': 5752 // IE returns default value 5753 if (v === 32768 || v === 2147483647 || v === '32768') 5754 v = ''; 5755 5756 break; 5757 5758 case 'multiple': 5759 case 'compact': 5760 case 'noshade': 5761 case 'nowrap': 5762 if (v === 65535) 5763 return n; 5764 5765 return dv; 5766 5767 case 'shape': 5768 v = v.toLowerCase(); 5769 break; 5770 5771 default: 5772 // IE has odd anonymous function for event attributes 5773 if (n.indexOf('on') === 0 && v) 5774 v = tinymce._replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1', '' + v); 5775 } 5776 } 5777 5778 return (v !== undef && v !== null && v !== '') ? '' + v : dv; 5779 }, 5780 5781 getPos : function(n, ro) { 5782 var t = this, x = 0, y = 0, e, d = t.doc, r; 5783 5784 n = t.get(n); 5785 ro = ro || d.body; 5786 5787 if (n) { 5788 // Use getBoundingClientRect if it exists since it's faster than looping offset nodes 5789 if (n.getBoundingClientRect) { 5790 n = n.getBoundingClientRect(); 5791 e = t.boxModel ? d.documentElement : d.body; 5792 5793 // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit 5794 // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position 5795 x = n.left + (d.documentElement.scrollLeft || d.body.scrollLeft) - e.clientTop; 5796 y = n.top + (d.documentElement.scrollTop || d.body.scrollTop) - e.clientLeft; 5797 5798 return {x : x, y : y}; 5799 } 5800 5801 r = n; 5802 while (r && r != ro && r.nodeType) { 5803 x += r.offsetLeft || 0; 5804 y += r.offsetTop || 0; 5805 r = r.offsetParent; 5806 } 5807 5808 r = n.parentNode; 5809 while (r && r != ro && r.nodeType) { 5810 x -= r.scrollLeft || 0; 5811 y -= r.scrollTop || 0; 5812 r = r.parentNode; 5813 } 5814 } 5815 5816 return {x : x, y : y}; 5817 }, 5818 5819 parseStyle : function(st) { 5820 return this.styles.parse(st); 5821 }, 5822 5823 serializeStyle : function(o, name) { 5824 return this.styles.serialize(o, name); 5825 }, 5826 5827 addStyle: function(cssText) { 5828 var doc = this.doc, head; 5829 5830 // Create style element if needed 5831 styleElm = doc.getElementById('mceDefaultStyles'); 5832 if (!styleElm) { 5833 styleElm = doc.createElement('style'), 5834 styleElm.id = 'mceDefaultStyles'; 5835 styleElm.type = 'text/css'; 5836 5837 head = doc.getElementsByTagName('head')[0] 5838 if (head.firstChild) { 5839 head.insertBefore(styleElm, head.firstChild); 5840 } else { 5841 head.appendChild(styleElm); 5842 } 5843 } 5844 5845 // Append style data to old or new style element 5846 if (styleElm.styleSheet) { 5847 styleElm.styleSheet.cssText += cssText; 5848 } else { 5849 styleElm.appendChild(doc.createTextNode(cssText)); 5850 } 5851 }, 5852 5853 loadCSS : function(u) { 5854 var t = this, d = t.doc, head; 5855 5856 if (!u) 5857 u = ''; 5858 5859 head = d.getElementsByTagName('head')[0]; 5860 5861 each(u.split(','), function(u) { 5862 var link; 5863 5864 if (t.files[u]) 5865 return; 5866 5867 t.files[u] = true; 5868 link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)}); 5869 5870 // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug 5871 // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading 5872 // It's ugly but it seems to work fine. 5873 if (isIE && d.documentMode && d.recalc) { 5874 link.onload = function() { 5875 if (d.recalc) 5876 d.recalc(); 5877 5878 link.onload = null; 5879 }; 5880 } 5881 5882 head.appendChild(link); 5883 }); 5884 }, 5885 5886 addClass : function(e, c) { 5887 return this.run(e, function(e) { 5888 var o; 5889 5890 if (!c) 5891 return 0; 5892 5893 if (this.hasClass(e, c)) 5894 return e.className; 5895 5896 o = this.removeClass(e, c); 5897 5898 return e.className = (o != '' ? (o + ' ') : '') + c; 5899 }); 5900 }, 5901 5902 removeClass : function(e, c) { 5903 var t = this, re; 5904 5905 return t.run(e, function(e) { 5906 var v; 5907 5908 if (t.hasClass(e, c)) { 5909 if (!re) 5910 re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g"); 5911 5912 v = e.className.replace(re, ' '); 5913 v = tinymce.trim(v != ' ' ? v : ''); 5914 5915 e.className = v; 5916 5917 // Empty class attr 5918 if (!v) { 5919 e.removeAttribute('class'); 5920 e.removeAttribute('className'); 5921 } 5922 5923 return v; 5924 } 5925 5926 return e.className; 5927 }); 5928 }, 5929 5930 hasClass : function(n, c) { 5931 n = this.get(n); 5932 5933 if (!n || !c) 5934 return false; 5935 5936 return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1; 5937 }, 5938 5939 show : function(e) { 5940 return this.setStyle(e, 'display', 'block'); 5941 }, 5942 5943 hide : function(e) { 5944 return this.setStyle(e, 'display', 'none'); 5945 }, 5946 5947 isHidden : function(e) { 5948 e = this.get(e); 5949 5950 return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none'; 5951 }, 5952 5953 uniqueId : function(p) { 5954 return (!p ? 'mce_' : p) + (this.counter++); 5955 }, 5956 5957 setHTML : function(element, html) { 5958 var self = this; 5959 5960 return self.run(element, function(element) { 5961 if (isIE) { 5962 // Remove all child nodes, IE keeps empty text nodes in DOM 5963 while (element.firstChild) 5964 element.removeChild(element.firstChild); 5965 5966 try { 5967 // IE will remove comments from the beginning 5968 // unless you padd the contents with something 5969 element.innerHTML = '<br />' + html; 5970 element.removeChild(element.firstChild); 5971 } catch (ex) { 5972 // IE sometimes produces an unknown runtime error on innerHTML if it's an block element within a block element for example a div inside a p 5973 // This seems to fix this problem 5974 5975 // Create new div with HTML contents and a BR infront to keep comments 5976 var newElement = self.create('div'); 5977 newElement.innerHTML = '<br />' + html; 5978 5979 // Add all children from div to target 5980 each (tinymce.grep(newElement.childNodes), function(node, i) { 5981 // Skip br element 5982 if (i && element.canHaveHTML) 5983 element.appendChild(node); 5984 }); 5985 } 5986 } else 5987 element.innerHTML = html; 5988 5989 return html; 5990 }); 5991 }, 5992 5993 getOuterHTML : function(elm) { 5994 var doc, self = this; 5995 5996 elm = self.get(elm); 5997 5998 if (!elm) 5999 return null; 6000 6001 if (elm.nodeType === 1 && self.hasOuterHTML) 6002 return elm.outerHTML; 6003 6004 doc = (elm.ownerDocument || self.doc).createElement("body"); 6005 doc.appendChild(elm.cloneNode(true)); 6006 6007 return doc.innerHTML; 6008 }, 6009 6010 setOuterHTML : function(e, h, d) { 6011 var t = this; 6012 6013 function setHTML(e, h, d) { 6014 var n, tp; 6015 6016 tp = d.createElement("body"); 6017 tp.innerHTML = h; 6018 6019 n = tp.lastChild; 6020 while (n) { 6021 t.insertAfter(n.cloneNode(true), e); 6022 n = n.previousSibling; 6023 } 6024 6025 t.remove(e); 6026 }; 6027 6028 return this.run(e, function(e) { 6029 e = t.get(e); 6030 6031 // Only set HTML on elements 6032 if (e.nodeType == 1) { 6033 d = d || e.ownerDocument || t.doc; 6034 6035 if (isIE) { 6036 try { 6037 // Try outerHTML for IE it sometimes produces an unknown runtime error 6038 if (isIE && e.nodeType == 1) 6039 e.outerHTML = h; 6040 else 6041 setHTML(e, h, d); 6042 } catch (ex) { 6043 // Fix for unknown runtime error 6044 setHTML(e, h, d); 6045 } 6046 } else 6047 setHTML(e, h, d); 6048 } 6049 }); 6050 }, 6051 6052 decode : Entities.decode, 6053 6054 encode : Entities.encodeAllRaw, 6055 6056 insertAfter : function(node, reference_node) { 6057 reference_node = this.get(reference_node); 6058 6059 return this.run(node, function(node) { 6060 var parent, nextSibling; 6061 6062 parent = reference_node.parentNode; 6063 nextSibling = reference_node.nextSibling; 6064 6065 if (nextSibling) 6066 parent.insertBefore(node, nextSibling); 6067 else 6068 parent.appendChild(node); 6069 6070 return node; 6071 }); 6072 }, 6073 6074 replace : function(n, o, k) { 6075 var t = this; 6076 6077 if (is(o, 'array')) 6078 n = n.cloneNode(true); 6079 6080 return t.run(o, function(o) { 6081 if (k) { 6082 each(tinymce.grep(o.childNodes), function(c) { 6083 n.appendChild(c); 6084 }); 6085 } 6086 6087 return o.parentNode.replaceChild(n, o); 6088 }); 6089 }, 6090 6091 rename : function(elm, name) { 6092 var t = this, newElm; 6093 6094 if (elm.nodeName != name.toUpperCase()) { 6095 // Rename block element 6096 newElm = t.create(name); 6097 6098 // Copy attribs to new block 6099 each(t.getAttribs(elm), function(attr_node) { 6100 t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName)); 6101 }); 6102 6103 // Replace block 6104 t.replace(newElm, elm, 1); 6105 } 6106 6107 return newElm || elm; 6108 }, 6109 6110 findCommonAncestor : function(a, b) { 6111 var ps = a, pe; 6112 6113 while (ps) { 6114 pe = b; 6115 6116 while (pe && ps != pe) 6117 pe = pe.parentNode; 6118 6119 if (ps == pe) 6120 break; 6121 6122 ps = ps.parentNode; 6123 } 6124 6125 if (!ps && a.ownerDocument) 6126 return a.ownerDocument.documentElement; 6127 6128 return ps; 6129 }, 6130 6131 toHex : function(s) { 6132 var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s); 6133 6134 function hex(s) { 6135 s = parseInt(s, 10).toString(16); 6136 6137 return s.length > 1 ? s : '0' + s; // 0 -> 00 6138 }; 6139 6140 if (c) { 6141 s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]); 6142 6143 return s; 6144 } 6145 6146 return s; 6147 }, 6148 6149 getClasses : function() { 6150 var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov; 6151 6152 if (t.classes) 6153 return t.classes; 6154 6155 function addClasses(s) { 6156 // IE style imports 6157 each(s.imports, function(r) { 6158 addClasses(r); 6159 }); 6160 6161 each(s.cssRules || s.rules, function(r) { 6162 // Real type or fake it on IE 6163 switch (r.type || 1) { 6164 // Rule 6165 case 1: 6166 if (r.selectorText) { 6167 each(r.selectorText.split(','), function(v) { 6168 v = v.replace(/^\s*|\s*$|^\s\./g, ""); 6169 6170 // Is internal or it doesn't contain a class 6171 if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v)) 6172 return; 6173 6174 // Remove everything but class name 6175 ov = v; 6176 v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v); 6177 6178 // Filter classes 6179 if (f && !(v = f(v, ov))) 6180 return; 6181 6182 if (!lo[v]) { 6183 cl.push({'class' : v}); 6184 lo[v] = 1; 6185 } 6186 }); 6187 } 6188 break; 6189 6190 // Import 6191 case 3: 6192 addClasses(r.styleSheet); 6193 break; 6194 } 6195 }); 6196 }; 6197 6198 try { 6199 each(t.doc.styleSheets, addClasses); 6200 } catch (ex) { 6201 // Ignore 6202 } 6203 6204 if (cl.length > 0) 6205 t.classes = cl; 6206 6207 return cl; 6208 }, 6209 6210 run : function(e, f, s) { 6211 var t = this, o; 6212 6213 if (t.doc && typeof(e) === 'string') 6214 e = t.get(e); 6215 6216 if (!e) 6217 return false; 6218 6219 s = s || this; 6220 if (!e.nodeType && (e.length || e.length === 0)) { 6221 o = []; 6222 6223 each(e, function(e, i) { 6224 if (e) { 6225 if (typeof(e) == 'string') 6226 e = t.doc.getElementById(e); 6227 6228 o.push(f.call(s, e, i)); 6229 } 6230 }); 6231 6232 return o; 6233 } 6234 6235 return f.call(s, e); 6236 }, 6237 6238 getAttribs : function(n) { 6239 var o; 6240 6241 n = this.get(n); 6242 6243 if (!n) 6244 return []; 6245 6246 if (isIE) { 6247 o = []; 6248 6249 // Object will throw exception in IE 6250 if (n.nodeName == 'OBJECT') 6251 return n.attributes; 6252 6253 // IE doesn't keep the selected attribute if you clone option elements 6254 if (n.nodeName === 'OPTION' && this.getAttrib(n, 'selected')) 6255 o.push({specified : 1, nodeName : 'selected'}); 6256 6257 // It's crazy that this is faster in IE but it's because it returns all attributes all the time 6258 n.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi, '').replace(/[\w:\-]+/gi, function(a) { 6259 o.push({specified : 1, nodeName : a}); 6260 }); 6261 6262 return o; 6263 } 6264 6265 return n.attributes; 6266 }, 6267 6268 isEmpty : function(node, elements) { 6269 var self = this, i, attributes, type, walker, name, brCount = 0; 6270 6271 node = node.firstChild; 6272 if (node) { 6273 walker = new tinymce.dom.TreeWalker(node, node.parentNode); 6274 elements = elements || self.schema ? self.schema.getNonEmptyElements() : null; 6275 6276 do { 6277 type = node.nodeType; 6278 6279 if (type === 1) { 6280 // Ignore bogus elements 6281 if (node.getAttribute('data-mce-bogus')) 6282 continue; 6283 6284 // Keep empty elements like <img /> 6285 name = node.nodeName.toLowerCase(); 6286 if (elements && elements[name]) { 6287 // Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p> 6288 if (name === 'br') { 6289 brCount++; 6290 continue; 6291 } 6292 6293 return false; 6294 } 6295 6296 // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a> 6297 attributes = self.getAttribs(node); 6298 i = node.attributes.length; 6299 while (i--) { 6300 name = node.attributes[i].nodeName; 6301 if (name === "name" || name === 'data-mce-bookmark') 6302 return false; 6303 } 6304 } 6305 6306 // Keep comment nodes 6307 if (type == 8) 6308 return false; 6309 6310 // Keep non whitespace text nodes 6311 if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) 6312 return false; 6313 } while (node = walker.next()); 6314 } 6315 6316 return brCount <= 1; 6317 }, 6318 6319 destroy : function(s) { 6320 var t = this; 6321 6322 t.win = t.doc = t.root = t.events = t.frag = null; 6323 6324 // Manual destroy then remove unload handler 6325 if (!s) 6326 tinymce.removeUnload(t.destroy); 6327 }, 6328 6329 createRng : function() { 6330 var d = this.doc; 6331 6332 return d.createRange ? d.createRange() : new tinymce.dom.Range(this); 6333 }, 6334 6335 nodeIndex : function(node, normalized) { 6336 var idx = 0, lastNodeType, lastNode, nodeType; 6337 6338 if (node) { 6339 for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { 6340 nodeType = node.nodeType; 6341 6342 // Normalize text nodes 6343 if (normalized && nodeType == 3) { 6344 if (nodeType == lastNodeType || !node.nodeValue.length) 6345 continue; 6346 } 6347 idx++; 6348 lastNodeType = nodeType; 6349 } 6350 } 6351 6352 return idx; 6353 }, 6354 6355 split : function(pe, e, re) { 6356 var t = this, r = t.createRng(), bef, aft, pa; 6357 6358 // W3C valid browsers tend to leave empty nodes to the left/right side of the contents, this makes sense 6359 // but we don't want that in our code since it serves no purpose for the end user 6360 // For example if this is chopped: 6361 // <p>text 1<span><b>CHOP</b></span>text 2</p> 6362 // would produce: 6363 // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p> 6364 // this function will then trim of empty edges and produce: 6365 // <p>text 1</p><b>CHOP</b><p>text 2</p> 6366 function trim(node) { 6367 var i, children = node.childNodes, type = node.nodeType; 6368 6369 function surroundedBySpans(node) { 6370 var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; 6371 var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; 6372 return previousIsSpan && nextIsSpan; 6373 } 6374 6375 if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') 6376 return; 6377 6378 for (i = children.length - 1; i >= 0; i--) 6379 trim(children[i]); 6380 6381 if (type != 9) { 6382 // Keep non whitespace text nodes 6383 if (type == 3 && node.nodeValue.length > 0) { 6384 // If parent element isn't a block or there isn't any useful contents for example "<p> </p>" 6385 // Also keep text nodes with only spaces if surrounded by spans. 6386 // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b 6387 var trimmedLength = tinymce.trim(node.nodeValue).length; 6388 if (!t.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) 6389 return; 6390 } else if (type == 1) { 6391 // If the only child is a bookmark then move it up 6392 children = node.childNodes; 6393 if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('data-mce-type') == 'bookmark') 6394 node.parentNode.insertBefore(children[0], node); 6395 6396 // Keep non empty elements or img, hr etc 6397 if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) 6398 return; 6399 } 6400 6401 t.remove(node); 6402 } 6403 6404 return node; 6405 }; 6406 6407 if (pe && e) { 6408 // Get before chunk 6409 r.setStart(pe.parentNode, t.nodeIndex(pe)); 6410 r.setEnd(e.parentNode, t.nodeIndex(e)); 6411 bef = r.extractContents(); 6412 6413 // Get after chunk 6414 r = t.createRng(); 6415 r.setStart(e.parentNode, t.nodeIndex(e) + 1); 6416 r.setEnd(pe.parentNode, t.nodeIndex(pe) + 1); 6417 aft = r.extractContents(); 6418 6419 // Insert before chunk 6420 pa = pe.parentNode; 6421 pa.insertBefore(trim(bef), pe); 6422 6423 // Insert middle chunk 6424 if (re) 6425 pa.replaceChild(re, e); 6426 else 6427 pa.insertBefore(e, pe); 6428 6429 // Insert after chunk 6430 pa.insertBefore(trim(aft), pe); 6431 t.remove(pe); 6432 6433 return re || e; 6434 } 6435 }, 6436 6437 bind : function(target, name, func, scope) { 6438 return this.events.add(target, name, func, scope || this); 6439 }, 6440 6441 unbind : function(target, name, func) { 6442 return this.events.remove(target, name, func); 6443 }, 6444 6445 fire : function(target, name, evt) { 6446 return this.events.fire(target, name, evt); 6447 }, 6448 6449 // Returns the content editable state of a node 6450 getContentEditable: function(node) { 6451 var contentEditable; 6452 6453 // Check type 6454 if (node.nodeType != 1) { 6455 return null; 6456 } 6457 6458 // Check for fake content editable 6459 contentEditable = node.getAttribute("data-mce-contenteditable"); 6460 if (contentEditable && contentEditable !== "inherit") { 6461 return contentEditable; 6462 } 6463 6464 // Check for real content editable 6465 return node.contentEditable !== "inherit" ? node.contentEditable : null; 6466 }, 6467 6468 6469 _findSib : function(node, selector, name) { 6470 var t = this, f = selector; 6471 6472 if (node) { 6473 // If expression make a function of it using is 6474 if (is(f, 'string')) { 6475 f = function(node) { 6476 return t.is(node, selector); 6477 }; 6478 } 6479 6480 // Loop all siblings 6481 for (node = node[name]; node; node = node[name]) { 6482 if (f(node)) 6483 return node; 6484 } 6485 } 6486 6487 return null; 6488 }, 6489 6490 _isRes : function(c) { 6491 // Is live resizble element 6492 return /^(top|left|bottom|right|width|height)/i.test(c) || /;\s*(top|left|bottom|right|width|height)/i.test(c); 6493 } 6494 6495 /* 6496 walk : function(n, f, s) { 6497 var d = this.doc, w; 6498 6499 if (d.createTreeWalker) { 6500 w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false); 6501 6502 while ((n = w.nextNode()) != null) 6503 f.call(s || this, n); 6504 } else 6505 tinymce.walk(n, f, 'childNodes', s); 6506 } 6507 */ 6508 6509 /* 6510 toRGB : function(s) { 6511 var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s); 6512 6513 if (c) { 6514 // #FFF -> #FFFFFF 6515 if (!is(c[3])) 6516 c[3] = c[2] = c[1]; 6517 6518 return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")"; 6519 } 6520 6521 return s; 6522 } 6523 */ 6524 }); 6525 6526 tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); 6527 })(tinymce); 6528 6529 (function(ns) { 6530 // Range constructor 6531 function Range(dom) { 6532 var t = this, 6533 doc = dom.doc, 6534 EXTRACT = 0, 6535 CLONE = 1, 6536 DELETE = 2, 6537 TRUE = true, 6538 FALSE = false, 6539 START_OFFSET = 'startOffset', 6540 START_CONTAINER = 'startContainer', 6541 END_CONTAINER = 'endContainer', 6542 END_OFFSET = 'endOffset', 6543 extend = tinymce.extend, 6544 nodeIndex = dom.nodeIndex; 6545 6546 extend(t, { 6547 // Inital states 6548 startContainer : doc, 6549 startOffset : 0, 6550 endContainer : doc, 6551 endOffset : 0, 6552 collapsed : TRUE, 6553 commonAncestorContainer : doc, 6554 6555 // Range constants 6556 START_TO_START : 0, 6557 START_TO_END : 1, 6558 END_TO_END : 2, 6559 END_TO_START : 3, 6560 6561 // Public methods 6562 setStart : setStart, 6563 setEnd : setEnd, 6564 setStartBefore : setStartBefore, 6565 setStartAfter : setStartAfter, 6566 setEndBefore : setEndBefore, 6567 setEndAfter : setEndAfter, 6568 collapse : collapse, 6569 selectNode : selectNode, 6570 selectNodeContents : selectNodeContents, 6571 compareBoundaryPoints : compareBoundaryPoints, 6572 deleteContents : deleteContents, 6573 extractContents : extractContents, 6574 cloneContents : cloneContents, 6575 insertNode : insertNode, 6576 surroundContents : surroundContents, 6577 cloneRange : cloneRange, 6578 toStringIE : toStringIE 6579 }); 6580 6581 function createDocumentFragment() { 6582 return doc.createDocumentFragment(); 6583 }; 6584 6585 function setStart(n, o) { 6586 _setEndPoint(TRUE, n, o); 6587 }; 6588 6589 function setEnd(n, o) { 6590 _setEndPoint(FALSE, n, o); 6591 }; 6592 6593 function setStartBefore(n) { 6594 setStart(n.parentNode, nodeIndex(n)); 6595 }; 6596 6597 function setStartAfter(n) { 6598 setStart(n.parentNode, nodeIndex(n) + 1); 6599 }; 6600 6601 function setEndBefore(n) { 6602 setEnd(n.parentNode, nodeIndex(n)); 6603 }; 6604 6605 function setEndAfter(n) { 6606 setEnd(n.parentNode, nodeIndex(n) + 1); 6607 }; 6608 6609 function collapse(ts) { 6610 if (ts) { 6611 t[END_CONTAINER] = t[START_CONTAINER]; 6612 t[END_OFFSET] = t[START_OFFSET]; 6613 } else { 6614 t[START_CONTAINER] = t[END_CONTAINER]; 6615 t[START_OFFSET] = t[END_OFFSET]; 6616 } 6617 6618 t.collapsed = TRUE; 6619 }; 6620 6621 function selectNode(n) { 6622 setStartBefore(n); 6623 setEndAfter(n); 6624 }; 6625 6626 function selectNodeContents(n) { 6627 setStart(n, 0); 6628 setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); 6629 }; 6630 6631 function compareBoundaryPoints(h, r) { 6632 var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET], 6633 rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset; 6634 6635 // Check START_TO_START 6636 if (h === 0) 6637 return _compareBoundaryPoints(sc, so, rsc, rso); 6638 6639 // Check START_TO_END 6640 if (h === 1) 6641 return _compareBoundaryPoints(ec, eo, rsc, rso); 6642 6643 // Check END_TO_END 6644 if (h === 2) 6645 return _compareBoundaryPoints(ec, eo, rec, reo); 6646 6647 // Check END_TO_START 6648 if (h === 3) 6649 return _compareBoundaryPoints(sc, so, rec, reo); 6650 }; 6651 6652 function deleteContents() { 6653 _traverse(DELETE); 6654 }; 6655 6656 function extractContents() { 6657 return _traverse(EXTRACT); 6658 }; 6659 6660 function cloneContents() { 6661 return _traverse(CLONE); 6662 }; 6663 6664 function insertNode(n) { 6665 var startContainer = this[START_CONTAINER], 6666 startOffset = this[START_OFFSET], nn, o; 6667 6668 // Node is TEXT_NODE or CDATA 6669 if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) { 6670 if (!startOffset) { 6671 // At the start of text 6672 startContainer.parentNode.insertBefore(n, startContainer); 6673 } else if (startOffset >= startContainer.nodeValue.length) { 6674 // At the end of text 6675 dom.insertAfter(n, startContainer); 6676 } else { 6677 // Middle, need to split 6678 nn = startContainer.splitText(startOffset); 6679 startContainer.parentNode.insertBefore(n, nn); 6680 } 6681 } else { 6682 // Insert element node 6683 if (startContainer.childNodes.length > 0) 6684 o = startContainer.childNodes[startOffset]; 6685 6686 if (o) 6687 startContainer.insertBefore(n, o); 6688 else 6689 startContainer.appendChild(n); 6690 } 6691 }; 6692 6693 function surroundContents(n) { 6694 var f = t.extractContents(); 6695 6696 t.insertNode(n); 6697 n.appendChild(f); 6698 t.selectNode(n); 6699 }; 6700 6701 function cloneRange() { 6702 return extend(new Range(dom), { 6703 startContainer : t[START_CONTAINER], 6704 startOffset : t[START_OFFSET], 6705 endContainer : t[END_CONTAINER], 6706 endOffset : t[END_OFFSET], 6707 collapsed : t.collapsed, 6708 commonAncestorContainer : t.commonAncestorContainer 6709 }); 6710 }; 6711 6712 // Private methods 6713 6714 function _getSelectedNode(container, offset) { 6715 var child; 6716 6717 if (container.nodeType == 3 /* TEXT_NODE */) 6718 return container; 6719 6720 if (offset < 0) 6721 return container; 6722 6723 child = container.firstChild; 6724 while (child && offset > 0) { 6725 --offset; 6726 child = child.nextSibling; 6727 } 6728 6729 if (child) 6730 return child; 6731 6732 return container; 6733 }; 6734 6735 function _isCollapsed() { 6736 return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]); 6737 }; 6738 6739 function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { 6740 var c, offsetC, n, cmnRoot, childA, childB; 6741 6742 // In the first case the boundary-points have the same container. A is before B 6743 // if its offset is less than the offset of B, A is equal to B if its offset is 6744 // equal to the offset of B, and A is after B if its offset is greater than the 6745 // offset of B. 6746 if (containerA == containerB) { 6747 if (offsetA == offsetB) 6748 return 0; // equal 6749 6750 if (offsetA < offsetB) 6751 return -1; // before 6752 6753 return 1; // after 6754 } 6755 6756 // In the second case a child node C of the container of A is an ancestor 6757 // container of B. In this case, A is before B if the offset of A is less than or 6758 // equal to the index of the child node C and A is after B otherwise. 6759 c = containerB; 6760 while (c && c.parentNode != containerA) 6761 c = c.parentNode; 6762 6763 if (c) { 6764 offsetC = 0; 6765 n = containerA.firstChild; 6766 6767 while (n != c && offsetC < offsetA) { 6768 offsetC++; 6769 n = n.nextSibling; 6770 } 6771 6772 if (offsetA <= offsetC) 6773 return -1; // before 6774 6775 return 1; // after 6776 } 6777 6778 // In the third case a child node C of the container of B is an ancestor container 6779 // of A. In this case, A is before B if the index of the child node C is less than 6780 // the offset of B and A is after B otherwise. 6781 c = containerA; 6782 while (c && c.parentNode != containerB) { 6783 c = c.parentNode; 6784 } 6785 6786 if (c) { 6787 offsetC = 0; 6788 n = containerB.firstChild; 6789 6790 while (n != c && offsetC < offsetB) { 6791 offsetC++; 6792 n = n.nextSibling; 6793 } 6794 6795 if (offsetC < offsetB) 6796 return -1; // before 6797 6798 return 1; // after 6799 } 6800 6801 // In the fourth case, none of three other cases hold: the containers of A and B 6802 // are siblings or descendants of sibling nodes. In this case, A is before B if 6803 // the container of A is before the container of B in a pre-order traversal of the 6804 // Ranges' context tree and A is after B otherwise. 6805 cmnRoot = dom.findCommonAncestor(containerA, containerB); 6806 childA = containerA; 6807 6808 while (childA && childA.parentNode != cmnRoot) 6809 childA = childA.parentNode; 6810 6811 if (!childA) 6812 childA = cmnRoot; 6813 6814 childB = containerB; 6815 while (childB && childB.parentNode != cmnRoot) 6816 childB = childB.parentNode; 6817 6818 if (!childB) 6819 childB = cmnRoot; 6820 6821 if (childA == childB) 6822 return 0; // equal 6823 6824 n = cmnRoot.firstChild; 6825 while (n) { 6826 if (n == childA) 6827 return -1; // before 6828 6829 if (n == childB) 6830 return 1; // after 6831 6832 n = n.nextSibling; 6833 } 6834 }; 6835 6836 function _setEndPoint(st, n, o) { 6837 var ec, sc; 6838 6839 if (st) { 6840 t[START_CONTAINER] = n; 6841 t[START_OFFSET] = o; 6842 } else { 6843 t[END_CONTAINER] = n; 6844 t[END_OFFSET] = o; 6845 } 6846 6847 // If one boundary-point of a Range is set to have a root container 6848 // other than the current one for the Range, the Range is collapsed to 6849 // the new position. This enforces the restriction that both boundary- 6850 // points of a Range must have the same root container. 6851 ec = t[END_CONTAINER]; 6852 while (ec.parentNode) 6853 ec = ec.parentNode; 6854 6855 sc = t[START_CONTAINER]; 6856 while (sc.parentNode) 6857 sc = sc.parentNode; 6858 6859 if (sc == ec) { 6860 // The start position of a Range is guaranteed to never be after the 6861 // end position. To enforce this restriction, if the start is set to 6862 // be at a position after the end, the Range is collapsed to that 6863 // position. 6864 if (_compareBoundaryPoints(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0) 6865 t.collapse(st); 6866 } else 6867 t.collapse(st); 6868 6869 t.collapsed = _isCollapsed(); 6870 t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]); 6871 }; 6872 6873 function _traverse(how) { 6874 var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; 6875 6876 if (t[START_CONTAINER] == t[END_CONTAINER]) 6877 return _traverseSameContainer(how); 6878 6879 for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 6880 if (p == t[START_CONTAINER]) 6881 return _traverseCommonStartContainer(c, how); 6882 6883 ++endContainerDepth; 6884 } 6885 6886 for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 6887 if (p == t[END_CONTAINER]) 6888 return _traverseCommonEndContainer(c, how); 6889 6890 ++startContainerDepth; 6891 } 6892 6893 depthDiff = startContainerDepth - endContainerDepth; 6894 6895 startNode = t[START_CONTAINER]; 6896 while (depthDiff > 0) { 6897 startNode = startNode.parentNode; 6898 depthDiff--; 6899 } 6900 6901 endNode = t[END_CONTAINER]; 6902 while (depthDiff < 0) { 6903 endNode = endNode.parentNode; 6904 depthDiff++; 6905 } 6906 6907 // ascend the ancestor hierarchy until we have a common parent. 6908 for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) { 6909 startNode = sp; 6910 endNode = ep; 6911 } 6912 6913 return _traverseCommonAncestors(startNode, endNode, how); 6914 }; 6915 6916 function _traverseSameContainer(how) { 6917 var frag, s, sub, n, cnt, sibling, xferNode, start, len; 6918 6919 if (how != DELETE) 6920 frag = createDocumentFragment(); 6921 6922 // If selection is empty, just return the fragment 6923 if (t[START_OFFSET] == t[END_OFFSET]) 6924 return frag; 6925 6926 // Text node needs special case handling 6927 if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) { 6928 // get the substring 6929 s = t[START_CONTAINER].nodeValue; 6930 sub = s.substring(t[START_OFFSET], t[END_OFFSET]); 6931 6932 // set the original text node to its new value 6933 if (how != CLONE) { 6934 n = t[START_CONTAINER]; 6935 start = t[START_OFFSET]; 6936 len = t[END_OFFSET] - t[START_OFFSET]; 6937 6938 if (start === 0 && len >= n.nodeValue.length - 1) { 6939 n.parentNode.removeChild(n); 6940 } else { 6941 n.deleteData(start, len); 6942 } 6943 6944 // Nothing is partially selected, so collapse to start point 6945 t.collapse(TRUE); 6946 } 6947 6948 if (how == DELETE) 6949 return; 6950 6951 if (sub.length > 0) { 6952 frag.appendChild(doc.createTextNode(sub)); 6953 } 6954 6955 return frag; 6956 } 6957 6958 // Copy nodes between the start/end offsets. 6959 n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]); 6960 cnt = t[END_OFFSET] - t[START_OFFSET]; 6961 6962 while (n && cnt > 0) { 6963 sibling = n.nextSibling; 6964 xferNode = _traverseFullySelected(n, how); 6965 6966 if (frag) 6967 frag.appendChild( xferNode ); 6968 6969 --cnt; 6970 n = sibling; 6971 } 6972 6973 // Nothing is partially selected, so collapse to start point 6974 if (how != CLONE) 6975 t.collapse(TRUE); 6976 6977 return frag; 6978 }; 6979 6980 function _traverseCommonStartContainer(endAncestor, how) { 6981 var frag, n, endIdx, cnt, sibling, xferNode; 6982 6983 if (how != DELETE) 6984 frag = createDocumentFragment(); 6985 6986 n = _traverseRightBoundary(endAncestor, how); 6987 6988 if (frag) 6989 frag.appendChild(n); 6990 6991 endIdx = nodeIndex(endAncestor); 6992 cnt = endIdx - t[START_OFFSET]; 6993 6994 if (cnt <= 0) { 6995 // Collapse to just before the endAncestor, which 6996 // is partially selected. 6997 if (how != CLONE) { 6998 t.setEndBefore(endAncestor); 6999 t.collapse(FALSE); 7000 } 7001 7002 return frag; 7003 } 7004 7005 n = endAncestor.previousSibling; 7006 while (cnt > 0) { 7007 sibling = n.previousSibling; 7008 xferNode = _traverseFullySelected(n, how); 7009 7010 if (frag) 7011 frag.insertBefore(xferNode, frag.firstChild); 7012 7013 --cnt; 7014 n = sibling; 7015 } 7016 7017 // Collapse to just before the endAncestor, which 7018 // is partially selected. 7019 if (how != CLONE) { 7020 t.setEndBefore(endAncestor); 7021 t.collapse(FALSE); 7022 } 7023 7024 return frag; 7025 }; 7026 7027 function _traverseCommonEndContainer(startAncestor, how) { 7028 var frag, startIdx, n, cnt, sibling, xferNode; 7029 7030 if (how != DELETE) 7031 frag = createDocumentFragment(); 7032 7033 n = _traverseLeftBoundary(startAncestor, how); 7034 if (frag) 7035 frag.appendChild(n); 7036 7037 startIdx = nodeIndex(startAncestor); 7038 ++startIdx; // Because we already traversed it 7039 7040 cnt = t[END_OFFSET] - startIdx; 7041 n = startAncestor.nextSibling; 7042 while (n && cnt > 0) { 7043 sibling = n.nextSibling; 7044 xferNode = _traverseFullySelected(n, how); 7045 7046 if (frag) 7047 frag.appendChild(xferNode); 7048 7049 --cnt; 7050 n = sibling; 7051 } 7052 7053 if (how != CLONE) { 7054 t.setStartAfter(startAncestor); 7055 t.collapse(TRUE); 7056 } 7057 7058 return frag; 7059 }; 7060 7061 function _traverseCommonAncestors(startAncestor, endAncestor, how) { 7062 var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling; 7063 7064 if (how != DELETE) 7065 frag = createDocumentFragment(); 7066 7067 n = _traverseLeftBoundary(startAncestor, how); 7068 if (frag) 7069 frag.appendChild(n); 7070 7071 commonParent = startAncestor.parentNode; 7072 startOffset = nodeIndex(startAncestor); 7073 endOffset = nodeIndex(endAncestor); 7074 ++startOffset; 7075 7076 cnt = endOffset - startOffset; 7077 sibling = startAncestor.nextSibling; 7078 7079 while (cnt > 0) { 7080 nextSibling = sibling.nextSibling; 7081 n = _traverseFullySelected(sibling, how); 7082 7083 if (frag) 7084 frag.appendChild(n); 7085 7086 sibling = nextSibling; 7087 --cnt; 7088 } 7089 7090 n = _traverseRightBoundary(endAncestor, how); 7091 7092 if (frag) 7093 frag.appendChild(n); 7094 7095 if (how != CLONE) { 7096 t.setStartAfter(startAncestor); 7097 t.collapse(TRUE); 7098 } 7099 7100 return frag; 7101 }; 7102 7103 function _traverseRightBoundary(root, how) { 7104 var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent, prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[END_CONTAINER]; 7105 7106 if (next == root) 7107 return _traverseNode(next, isFullySelected, FALSE, how); 7108 7109 parent = next.parentNode; 7110 clonedParent = _traverseNode(parent, FALSE, FALSE, how); 7111 7112 while (parent) { 7113 while (next) { 7114 prevSibling = next.previousSibling; 7115 clonedChild = _traverseNode(next, isFullySelected, FALSE, how); 7116 7117 if (how != DELETE) 7118 clonedParent.insertBefore(clonedChild, clonedParent.firstChild); 7119 7120 isFullySelected = TRUE; 7121 next = prevSibling; 7122 } 7123 7124 if (parent == root) 7125 return clonedParent; 7126 7127 next = parent.previousSibling; 7128 parent = parent.parentNode; 7129 7130 clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); 7131 7132 if (how != DELETE) 7133 clonedGrandParent.appendChild(clonedParent); 7134 7135 clonedParent = clonedGrandParent; 7136 } 7137 }; 7138 7139 function _traverseLeftBoundary(root, how) { 7140 var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; 7141 7142 if (next == root) 7143 return _traverseNode(next, isFullySelected, TRUE, how); 7144 7145 parent = next.parentNode; 7146 clonedParent = _traverseNode(parent, FALSE, TRUE, how); 7147 7148 while (parent) { 7149 while (next) { 7150 nextSibling = next.nextSibling; 7151 clonedChild = _traverseNode(next, isFullySelected, TRUE, how); 7152 7153 if (how != DELETE) 7154 clonedParent.appendChild(clonedChild); 7155 7156 isFullySelected = TRUE; 7157 next = nextSibling; 7158 } 7159 7160 if (parent == root) 7161 return clonedParent; 7162 7163 next = parent.nextSibling; 7164 parent = parent.parentNode; 7165 7166 clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); 7167 7168 if (how != DELETE) 7169 clonedGrandParent.appendChild(clonedParent); 7170 7171 clonedParent = clonedGrandParent; 7172 } 7173 }; 7174 7175 function _traverseNode(n, isFullySelected, isLeft, how) { 7176 var txtValue, newNodeValue, oldNodeValue, offset, newNode; 7177 7178 if (isFullySelected) 7179 return _traverseFullySelected(n, how); 7180 7181 if (n.nodeType == 3 /* TEXT_NODE */) { 7182 txtValue = n.nodeValue; 7183 7184 if (isLeft) { 7185 offset = t[START_OFFSET]; 7186 newNodeValue = txtValue.substring(offset); 7187 oldNodeValue = txtValue.substring(0, offset); 7188 } else { 7189 offset = t[END_OFFSET]; 7190 newNodeValue = txtValue.substring(0, offset); 7191 oldNodeValue = txtValue.substring(offset); 7192 } 7193 7194 if (how != CLONE) 7195 n.nodeValue = oldNodeValue; 7196 7197 if (how == DELETE) 7198 return; 7199 7200 newNode = dom.clone(n, FALSE); 7201 newNode.nodeValue = newNodeValue; 7202 7203 return newNode; 7204 } 7205 7206 if (how == DELETE) 7207 return; 7208 7209 return dom.clone(n, FALSE); 7210 }; 7211 7212 function _traverseFullySelected(n, how) { 7213 if (how != DELETE) 7214 return how == CLONE ? dom.clone(n, TRUE) : n; 7215 7216 n.parentNode.removeChild(n); 7217 }; 7218 7219 function toStringIE() { 7220 return dom.create('body', null, cloneContents()).outerText; 7221 } 7222 7223 return t; 7224 }; 7225 7226 ns.Range = Range; 7227 7228 // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype 7229 Range.prototype.toString = function() { 7230 return this.toStringIE(); 7231 }; 7232 })(tinymce.dom); 7233 7234 (function() { 7235 function Selection(selection) { 7236 var self = this, dom = selection.dom, TRUE = true, FALSE = false; 7237 7238 function getPosition(rng, start) { 7239 var checkRng, startIndex = 0, endIndex, inside, 7240 children, child, offset, index, position = -1, parent; 7241 7242 // Setup test range, collapse it and get the parent 7243 checkRng = rng.duplicate(); 7244 checkRng.collapse(start); 7245 parent = checkRng.parentElement(); 7246 7247 // Check if the selection is within the right document 7248 if (parent.ownerDocument !== selection.dom.doc) 7249 return; 7250 7251 // IE will report non editable elements as it's parent so look for an editable one 7252 while (parent.contentEditable === "false") { 7253 parent = parent.parentNode; 7254 } 7255 7256 // If parent doesn't have any children then return that we are inside the element 7257 if (!parent.hasChildNodes()) { 7258 return {node : parent, inside : 1}; 7259 } 7260 7261 // Setup node list and endIndex 7262 children = parent.children; 7263 endIndex = children.length - 1; 7264 7265 // Perform a binary search for the position 7266 while (startIndex <= endIndex) { 7267 index = Math.floor((startIndex + endIndex) / 2); 7268 7269 // Move selection to node and compare the ranges 7270 child = children[index]; 7271 checkRng.moveToElementText(child); 7272 position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); 7273 7274 // Before/after or an exact match 7275 if (position > 0) { 7276 endIndex = index - 1; 7277 } else if (position < 0) { 7278 startIndex = index + 1; 7279 } else { 7280 return {node : child}; 7281 } 7282 } 7283 7284 // Check if child position is before or we didn't find a position 7285 if (position < 0) { 7286 // No element child was found use the parent element and the offset inside that 7287 if (!child) { 7288 checkRng.moveToElementText(parent); 7289 checkRng.collapse(true); 7290 child = parent; 7291 inside = true; 7292 } else 7293 checkRng.collapse(false); 7294 7295 // Walk character by character in text node until we hit the selected range endpoint, hit the end of document or parent isn't the right one 7296 // We need to walk char by char since rng.text or rng.htmlText will trim line endings 7297 offset = 0; 7298 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 7299 if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) { 7300 break; 7301 } 7302 7303 offset++; 7304 } 7305 } else { 7306 // Child position is after the selection endpoint 7307 checkRng.collapse(true); 7308 7309 // Walk character by character in text node until we hit the selected range endpoint, hit the end of document or parent isn't the right one 7310 offset = 0; 7311 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 7312 if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) { 7313 break; 7314 } 7315 7316 offset++; 7317 } 7318 } 7319 7320 return {node : child, position : position, offset : offset, inside : inside}; 7321 }; 7322 7323 // Returns a W3C DOM compatible range object by using the IE Range API 7324 function getRange() { 7325 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail; 7326 7327 // If selection is outside the current document just return an empty range 7328 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); 7329 if (element.ownerDocument != dom.doc) 7330 return domRange; 7331 7332 collapsed = selection.isCollapsed(); 7333 7334 // Handle control selection 7335 if (ieRange.item) { 7336 domRange.setStart(element.parentNode, dom.nodeIndex(element)); 7337 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); 7338 7339 return domRange; 7340 } 7341 7342 function findEndPoint(start) { 7343 var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; 7344 7345 container = endPoint.node; 7346 offset = endPoint.offset; 7347 7348 if (endPoint.inside && !container.hasChildNodes()) { 7349 domRange[start ? 'setStart' : 'setEnd'](container, 0); 7350 return; 7351 } 7352 7353 if (offset === undef) { 7354 domRange[start ? 'setStartBefore' : 'setEndAfter'](container); 7355 return; 7356 } 7357 7358 if (endPoint.position < 0) { 7359 sibling = endPoint.inside ? container.firstChild : container.nextSibling; 7360 7361 if (!sibling) { 7362 domRange[start ? 'setStartAfter' : 'setEndAfter'](container); 7363 return; 7364 } 7365 7366 if (!offset) { 7367 if (sibling.nodeType == 3) 7368 domRange[start ? 'setStart' : 'setEnd'](sibling, 0); 7369 else 7370 domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); 7371 7372 return; 7373 } 7374 7375 // Find the text node and offset 7376 while (sibling) { 7377 nodeValue = sibling.nodeValue; 7378 textNodeOffset += nodeValue.length; 7379 7380 // We are at or passed the position we where looking for 7381 if (textNodeOffset >= offset) { 7382 container = sibling; 7383 textNodeOffset -= offset; 7384 textNodeOffset = nodeValue.length - textNodeOffset; 7385 break; 7386 } 7387 7388 sibling = sibling.nextSibling; 7389 } 7390 } else { 7391 // Find the text node and offset 7392 sibling = container.previousSibling; 7393 7394 if (!sibling) 7395 return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); 7396 7397 // If there isn't any text to loop then use the first position 7398 if (!offset) { 7399 if (container.nodeType == 3) 7400 domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); 7401 else 7402 domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); 7403 7404 return; 7405 } 7406 7407 while (sibling) { 7408 textNodeOffset += sibling.nodeValue.length; 7409 7410 // We are at or passed the position we where looking for 7411 if (textNodeOffset >= offset) { 7412 container = sibling; 7413 textNodeOffset -= offset; 7414 break; 7415 } 7416 7417 sibling = sibling.previousSibling; 7418 } 7419 } 7420 7421 domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); 7422 }; 7423 7424 try { 7425 // Find start point 7426 findEndPoint(true); 7427 7428 // Find end point if needed 7429 if (!collapsed) 7430 findEndPoint(); 7431 } catch (ex) { 7432 // IE has a nasty bug where text nodes might throw "invalid argument" when you 7433 // access the nodeValue or other properties of text nodes. This seems to happend when 7434 // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. 7435 if (ex.number == -2147024809) { 7436 // Get the current selection 7437 bookmark = self.getBookmark(2); 7438 7439 // Get start element 7440 tmpRange = ieRange.duplicate(); 7441 tmpRange.collapse(true); 7442 element = tmpRange.parentElement(); 7443 7444 // Get end element 7445 if (!collapsed) { 7446 tmpRange = ieRange.duplicate(); 7447 tmpRange.collapse(false); 7448 element2 = tmpRange.parentElement(); 7449 element2.innerHTML = element2.innerHTML; 7450 } 7451 7452 // Remove the broken elements 7453 element.innerHTML = element.innerHTML; 7454 7455 // Restore the selection 7456 self.moveToBookmark(bookmark); 7457 7458 // Since the range has moved we need to re-get it 7459 ieRange = selection.getRng(); 7460 7461 // Find start point 7462 findEndPoint(true); 7463 7464 // Find end point if needed 7465 if (!collapsed) 7466 findEndPoint(); 7467 } else 7468 throw ex; // Throw other errors 7469 } 7470 7471 return domRange; 7472 }; 7473 7474 this.getBookmark = function(type) { 7475 var rng = selection.getRng(), start, end, bookmark = {}; 7476 7477 function getIndexes(node) { 7478 var parent, root, children, i, indexes = []; 7479 7480 parent = node.parentNode; 7481 root = dom.getRoot().parentNode; 7482 7483 while (parent != root && parent.nodeType !== 9) { 7484 children = parent.children; 7485 7486 i = children.length; 7487 while (i--) { 7488 if (node === children[i]) { 7489 indexes.push(i); 7490 break; 7491 } 7492 } 7493 7494 node = parent; 7495 parent = parent.parentNode; 7496 } 7497 7498 return indexes; 7499 }; 7500 7501 function getBookmarkEndPoint(start) { 7502 var position; 7503 7504 position = getPosition(rng, start); 7505 if (position) { 7506 return { 7507 position : position.position, 7508 offset : position.offset, 7509 indexes : getIndexes(position.node), 7510 inside : position.inside 7511 }; 7512 } 7513 }; 7514 7515 // Non ubstructive bookmark 7516 if (type === 2) { 7517 // Handle text selection 7518 if (!rng.item) { 7519 bookmark.start = getBookmarkEndPoint(true); 7520 7521 if (!selection.isCollapsed()) 7522 bookmark.end = getBookmarkEndPoint(); 7523 } else 7524 bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))}; 7525 } 7526 7527 return bookmark; 7528 }; 7529 7530 this.moveToBookmark = function(bookmark) { 7531 var rng, body = dom.doc.body; 7532 7533 function resolveIndexes(indexes) { 7534 var node, i, idx, children; 7535 7536 node = dom.getRoot(); 7537 for (i = indexes.length - 1; i >= 0; i--) { 7538 children = node.children; 7539 idx = indexes[i]; 7540 7541 if (idx <= children.length - 1) { 7542 node = children[idx]; 7543 } 7544 } 7545 7546 return node; 7547 }; 7548 7549 function setBookmarkEndPoint(start) { 7550 var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef; 7551 7552 if (endPoint) { 7553 moveLeft = endPoint.position > 0; 7554 7555 moveRng = body.createTextRange(); 7556 moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); 7557 7558 offset = endPoint.offset; 7559 if (offset !== undef) { 7560 moveRng.collapse(endPoint.inside || moveLeft); 7561 moveRng.moveStart('character', moveLeft ? -offset : offset); 7562 } else 7563 moveRng.collapse(start); 7564 7565 rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); 7566 7567 if (start) 7568 rng.collapse(true); 7569 } 7570 }; 7571 7572 if (bookmark.start) { 7573 if (bookmark.start.ctrl) { 7574 rng = body.createControlRange(); 7575 rng.addElement(resolveIndexes(bookmark.start.indexes)); 7576 rng.select(); 7577 } else { 7578 rng = body.createTextRange(); 7579 setBookmarkEndPoint(true); 7580 setBookmarkEndPoint(); 7581 rng.select(); 7582 } 7583 } 7584 }; 7585 7586 this.addRange = function(rng) { 7587 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, doc = selection.dom.doc, body = doc.body; 7588 7589 function setEndPoint(start) { 7590 var container, offset, marker, tmpRng, nodes; 7591 7592 marker = dom.create('a'); 7593 container = start ? startContainer : endContainer; 7594 offset = start ? startOffset : endOffset; 7595 tmpRng = ieRng.duplicate(); 7596 7597 if (container == doc || container == doc.documentElement) { 7598 container = body; 7599 offset = 0; 7600 } 7601 7602 if (container.nodeType == 3) { 7603 container.parentNode.insertBefore(marker, container); 7604 tmpRng.moveToElementText(marker); 7605 tmpRng.moveStart('character', offset); 7606 dom.remove(marker); 7607 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 7608 } else { 7609 nodes = container.childNodes; 7610 7611 if (nodes.length) { 7612 if (offset >= nodes.length) { 7613 dom.insertAfter(marker, nodes[nodes.length - 1]); 7614 } else { 7615 container.insertBefore(marker, nodes[offset]); 7616 } 7617 7618 tmpRng.moveToElementText(marker); 7619 } else if (container.canHaveHTML) { 7620 // Empty node selection for example <div>|</div> 7621 // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open 7622 container.innerHTML = '<span>\uFEFF</span>'; 7623 marker = container.firstChild; 7624 tmpRng.moveToElementText(marker); 7625 tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason 7626 } 7627 7628 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 7629 dom.remove(marker); 7630 } 7631 } 7632 7633 // Setup some shorter versions 7634 startContainer = rng.startContainer; 7635 startOffset = rng.startOffset; 7636 endContainer = rng.endContainer; 7637 endOffset = rng.endOffset; 7638 ieRng = body.createTextRange(); 7639 7640 // If single element selection then try making a control selection out of it 7641 if (startContainer == endContainer && startContainer.nodeType == 1) { 7642 // Trick to place the caret inside an empty block element like <p></p> 7643 if (startOffset == endOffset && !startContainer.hasChildNodes()) { 7644 if (startContainer.canHaveHTML) { 7645 // Check if previous sibling is an empty block if it is then we need to render it 7646 // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236 7647 // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p> 7648 sibling = startContainer.previousSibling; 7649 if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { 7650 sibling.innerHTML = '\uFEFF'; 7651 } else { 7652 sibling = null; 7653 } 7654 7655 startContainer.innerHTML = '<span>\uFEFF</span><span>\uFEFF</span>'; 7656 ieRng.moveToElementText(startContainer.lastChild); 7657 ieRng.select(); 7658 dom.doc.selection.clear(); 7659 startContainer.innerHTML = ''; 7660 7661 if (sibling) { 7662 sibling.innerHTML = ''; 7663 } 7664 return; 7665 } else { 7666 startOffset = dom.nodeIndex(startContainer); 7667 startContainer = startContainer.parentNode; 7668 } 7669 } 7670 7671 if (startOffset == endOffset - 1) { 7672 try { 7673 ctrlRng = body.createControlRange(); 7674 ctrlRng.addElement(startContainer.childNodes[startOffset]); 7675 ctrlRng.select(); 7676 return; 7677 } catch (ex) { 7678 // Ignore 7679 } 7680 } 7681 } 7682 7683 // Set start/end point of selection 7684 setEndPoint(true); 7685 setEndPoint(); 7686 7687 // Select the new range and scroll it into view 7688 ieRng.select(); 7689 }; 7690 7691 // Expose range method 7692 this.getRangeAt = getRange; 7693 }; 7694 7695 // Expose the selection object 7696 tinymce.dom.TridentSelection = Selection; 7697 })(); 7698 7699 7700 (function(tinymce) { 7701 tinymce.dom.Element = function(id, settings) { 7702 var t = this, dom, el; 7703 7704 t.settings = settings = settings || {}; 7705 t.id = id; 7706 t.dom = dom = settings.dom || tinymce.DOM; 7707 7708 // Only IE leaks DOM references, this is a lot faster 7709 if (!tinymce.isIE) 7710 el = dom.get(t.id); 7711 7712 tinymce.each( 7713 ('getPos,getRect,getParent,add,setStyle,getStyle,setStyles,' + 7714 'setAttrib,setAttribs,getAttrib,addClass,removeClass,' + 7715 'hasClass,getOuterHTML,setOuterHTML,remove,show,hide,' + 7716 'isHidden,setHTML,get').split(/,/), function(k) { 7717 t[k] = function() { 7718 var a = [id], i; 7719 7720 for (i = 0; i < arguments.length; i++) 7721 a.push(arguments[i]); 7722 7723 a = dom[k].apply(dom, a); 7724 t.update(k); 7725 7726 return a; 7727 }; 7728 } 7729 ); 7730 7731 tinymce.extend(t, { 7732 on : function(n, f, s) { 7733 return tinymce.dom.Event.add(t.id, n, f, s); 7734 }, 7735 7736 getXY : function() { 7737 return { 7738 x : parseInt(t.getStyle('left')), 7739 y : parseInt(t.getStyle('top')) 7740 }; 7741 }, 7742 7743 getSize : function() { 7744 var n = dom.get(t.id); 7745 7746 return { 7747 w : parseInt(t.getStyle('width') || n.clientWidth), 7748 h : parseInt(t.getStyle('height') || n.clientHeight) 7749 }; 7750 }, 7751 7752 moveTo : function(x, y) { 7753 t.setStyles({left : x, top : y}); 7754 }, 7755 7756 moveBy : function(x, y) { 7757 var p = t.getXY(); 7758 7759 t.moveTo(p.x + x, p.y + y); 7760 }, 7761 7762 resizeTo : function(w, h) { 7763 t.setStyles({width : w, height : h}); 7764 }, 7765 7766 resizeBy : function(w, h) { 7767 var s = t.getSize(); 7768 7769 t.resizeTo(s.w + w, s.h + h); 7770 }, 7771 7772 update : function(k) { 7773 var b; 7774 7775 if (tinymce.isIE6 && settings.blocker) { 7776 k = k || ''; 7777 7778 // Ignore getters 7779 if (k.indexOf('get') === 0 || k.indexOf('has') === 0 || k.indexOf('is') === 0) 7780 return; 7781 7782 // Remove blocker on remove 7783 if (k == 'remove') { 7784 dom.remove(t.blocker); 7785 return; 7786 } 7787 7788 if (!t.blocker) { 7789 t.blocker = dom.uniqueId(); 7790 b = dom.add(settings.container || dom.getRoot(), 'iframe', {id : t.blocker, style : 'position:absolute;', frameBorder : 0, src : 'javascript:""'}); 7791 dom.setStyle(b, 'opacity', 0); 7792 } else 7793 b = dom.get(t.blocker); 7794 7795 dom.setStyles(b, { 7796 left : t.getStyle('left', 1), 7797 top : t.getStyle('top', 1), 7798 width : t.getStyle('width', 1), 7799 height : t.getStyle('height', 1), 7800 display : t.getStyle('display', 1), 7801 zIndex : parseInt(t.getStyle('zIndex', 1) || 0) - 1 7802 }); 7803 } 7804 } 7805 }); 7806 }; 7807 })(tinymce); 7808 7809 (function(tinymce) { 7810 function trimNl(s) { 7811 return s.replace(/[\n\r]+/g, ''); 7812 }; 7813 7814 // Shorten names 7815 var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each, TreeWalker = tinymce.dom.TreeWalker; 7816 7817 tinymce.create('tinymce.dom.Selection', { 7818 Selection : function(dom, win, serializer, editor) { 7819 var t = this; 7820 7821 t.dom = dom; 7822 t.win = win; 7823 t.serializer = serializer; 7824 t.editor = editor; 7825 7826 // Add events 7827 each([ 7828 'onBeforeSetContent', 7829 7830 'onBeforeGetContent', 7831 7832 'onSetContent', 7833 7834 'onGetContent' 7835 ], function(e) { 7836 t[e] = new tinymce.util.Dispatcher(t); 7837 }); 7838 7839 // No W3C Range support 7840 if (!t.win.getSelection) 7841 t.tridentSel = new tinymce.dom.TridentSelection(t); 7842 7843 if (tinymce.isIE && dom.boxModel) 7844 this._fixIESelection(); 7845 7846 // Prevent leaks 7847 tinymce.addUnload(t.destroy, t); 7848 }, 7849 7850 setCursorLocation: function(node, offset) { 7851 var t = this; var r = t.dom.createRng(); 7852 r.setStart(node, offset); 7853 r.setEnd(node, offset); 7854 t.setRng(r); 7855 t.collapse(false); 7856 }, 7857 getContent : function(s) { 7858 var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n; 7859 7860 s = s || {}; 7861 wb = wa = ''; 7862 s.get = true; 7863 s.format = s.format || 'html'; 7864 s.forced_root_block = ''; 7865 t.onBeforeGetContent.dispatch(t, s); 7866 7867 if (s.format == 'text') 7868 return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : '')); 7869 7870 if (r.cloneContents) { 7871 n = r.cloneContents(); 7872 7873 if (n) 7874 e.appendChild(n); 7875 } else if (is(r.item) || is(r.htmlText)) { 7876 // IE will produce invalid markup if elements are present that 7877 // it doesn't understand like custom elements or HTML5 elements. 7878 // Adding a BR in front of the contents and then remoiving it seems to fix it though. 7879 e.innerHTML = '<br>' + (r.item ? r.item(0).outerHTML : r.htmlText); 7880 e.removeChild(e.firstChild); 7881 } else 7882 e.innerHTML = r.toString(); 7883 7884 // Keep whitespace before and after 7885 if (/^\s/.test(e.innerHTML)) 7886 wb = ' '; 7887 7888 if (/\s+$/.test(e.innerHTML)) 7889 wa = ' '; 7890 7891 s.getInner = true; 7892 7893 s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa; 7894 t.onGetContent.dispatch(t, s); 7895 7896 return s.content; 7897 }, 7898 7899 setContent : function(content, args) { 7900 var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp; 7901 7902 args = args || {format : 'html'}; 7903 args.set = true; 7904 content = args.content = content; 7905 7906 // Dispatch before set content event 7907 if (!args.no_events) 7908 self.onBeforeSetContent.dispatch(self, args); 7909 7910 content = args.content; 7911 7912 if (rng.insertNode) { 7913 // Make caret marker since insertNode places the caret in the beginning of text after insert 7914 content += '<span id="__caret">_</span>'; 7915 7916 // Delete and insert new node 7917 if (rng.startContainer == doc && rng.endContainer == doc) { 7918 // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents 7919 doc.body.innerHTML = content; 7920 } else { 7921 rng.deleteContents(); 7922 7923 if (doc.body.childNodes.length === 0) { 7924 doc.body.innerHTML = content; 7925 } else { 7926 // createContextualFragment doesn't exists in IE 9 DOMRanges 7927 if (rng.createContextualFragment) { 7928 rng.insertNode(rng.createContextualFragment(content)); 7929 } else { 7930 // Fake createContextualFragment call in IE 9 7931 frag = doc.createDocumentFragment(); 7932 temp = doc.createElement('div'); 7933 7934 frag.appendChild(temp); 7935 temp.outerHTML = content; 7936 7937 rng.insertNode(frag); 7938 } 7939 } 7940 } 7941 7942 // Move to caret marker 7943 caretNode = self.dom.get('__caret'); 7944 7945 // Make sure we wrap it compleatly, Opera fails with a simple select call 7946 rng = doc.createRange(); 7947 rng.setStartBefore(caretNode); 7948 rng.setEndBefore(caretNode); 7949 self.setRng(rng); 7950 7951 // Remove the caret position 7952 self.dom.remove('__caret'); 7953 7954 try { 7955 self.setRng(rng); 7956 } catch (ex) { 7957 // Might fail on Opera for some odd reason 7958 } 7959 } else { 7960 if (rng.item) { 7961 // Delete content and get caret text selection 7962 doc.execCommand('Delete', false, null); 7963 rng = self.getRng(); 7964 } 7965 7966 // Explorer removes spaces from the beginning of pasted contents 7967 if (/^\s+/.test(content)) { 7968 rng.pasteHTML('<span id="__mce_tmp">_</span>' + content); 7969 self.dom.remove('__mce_tmp'); 7970 } else 7971 rng.pasteHTML(content); 7972 } 7973 7974 // Dispatch set content event 7975 if (!args.no_events) 7976 self.onSetContent.dispatch(self, args); 7977 }, 7978 7979 getStart : function() { 7980 var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node; 7981 7982 if (rng.duplicate || rng.item) { 7983 // Control selection, return first item 7984 if (rng.item) 7985 return rng.item(0); 7986 7987 // Get start element 7988 checkRng = rng.duplicate(); 7989 checkRng.collapse(1); 7990 startElement = checkRng.parentElement(); 7991 if (startElement.ownerDocument !== self.dom.doc) { 7992 startElement = self.dom.getRoot(); 7993 } 7994 7995 // Check if range parent is inside the start element, then return the inner parent element 7996 // This will fix issues when a single element is selected, IE would otherwise return the wrong start element 7997 parentElement = node = rng.parentElement(); 7998 while (node = node.parentNode) { 7999 if (node == startElement) { 8000 startElement = parentElement; 8001 break; 8002 } 8003 } 8004 8005 return startElement; 8006 } else { 8007 startElement = rng.startContainer; 8008 8009 if (startElement.nodeType == 1 && startElement.hasChildNodes()) 8010 startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; 8011 8012 if (startElement && startElement.nodeType == 3) 8013 return startElement.parentNode; 8014 8015 return startElement; 8016 } 8017 }, 8018 8019 getEnd : function() { 8020 var self = this, rng = self.getRng(), endElement, endOffset; 8021 8022 if (rng.duplicate || rng.item) { 8023 if (rng.item) 8024 return rng.item(0); 8025 8026 rng = rng.duplicate(); 8027 rng.collapse(0); 8028 endElement = rng.parentElement(); 8029 if (endElement.ownerDocument !== self.dom.doc) { 8030 endElement = self.dom.getRoot(); 8031 } 8032 8033 if (endElement && endElement.nodeName == 'BODY') 8034 return endElement.lastChild || endElement; 8035 8036 return endElement; 8037 } else { 8038 endElement = rng.endContainer; 8039 endOffset = rng.endOffset; 8040 8041 if (endElement.nodeType == 1 && endElement.hasChildNodes()) 8042 endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset]; 8043 8044 if (endElement && endElement.nodeType == 3) 8045 return endElement.parentNode; 8046 8047 return endElement; 8048 } 8049 }, 8050 8051 getBookmark : function(type, normalized) { 8052 var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles; 8053 8054 function findIndex(name, element) { 8055 var index = 0; 8056 8057 each(dom.select(name), function(node, i) { 8058 if (node == element) 8059 index = i; 8060 }); 8061 8062 return index; 8063 }; 8064 8065 function normalizeTableCellSelection(rng) { 8066 function moveEndPoint(start) { 8067 var container, offset, childNodes, prefix = start ? 'start' : 'end'; 8068 8069 container = rng[prefix + 'Container']; 8070 offset = rng[prefix + 'Offset']; 8071 8072 if (container.nodeType == 1 && container.nodeName == "TR") { 8073 childNodes = container.childNodes; 8074 container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; 8075 if (container) { 8076 offset = start ? 0 : container.childNodes.length; 8077 rng['set' + (start ? 'Start' : 'End')](container, offset); 8078 } 8079 } 8080 }; 8081 8082 moveEndPoint(true); 8083 moveEndPoint(); 8084 8085 return rng; 8086 }; 8087 8088 function getLocation() { 8089 var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; 8090 8091 function getPoint(rng, start) { 8092 var container = rng[start ? 'startContainer' : 'endContainer'], 8093 offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; 8094 8095 if (container.nodeType == 3) { 8096 if (normalized) { 8097 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) 8098 offset += node.nodeValue.length; 8099 } 8100 8101 point.push(offset); 8102 } else { 8103 childNodes = container.childNodes; 8104 8105 if (offset >= childNodes.length && childNodes.length) { 8106 after = 1; 8107 offset = Math.max(0, childNodes.length - 1); 8108 } 8109 8110 point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); 8111 } 8112 8113 for (; container && container != root; container = container.parentNode) 8114 point.push(t.dom.nodeIndex(container, normalized)); 8115 8116 return point; 8117 }; 8118 8119 bookmark.start = getPoint(rng, true); 8120 8121 if (!t.isCollapsed()) 8122 bookmark.end = getPoint(rng); 8123 8124 return bookmark; 8125 }; 8126 8127 if (type == 2) { 8128 if (t.tridentSel) 8129 return t.tridentSel.getBookmark(type); 8130 8131 return getLocation(); 8132 } 8133 8134 // Handle simple range 8135 if (type) 8136 return {rng : t.getRng()}; 8137 8138 rng = t.getRng(); 8139 id = dom.uniqueId(); 8140 collapsed = tinyMCE.activeEditor.selection.isCollapsed(); 8141 styles = 'overflow:hidden;line-height:0px'; 8142 8143 // Explorer method 8144 if (rng.duplicate || rng.item) { 8145 // Text selection 8146 if (!rng.item) { 8147 rng2 = rng.duplicate(); 8148 8149 try { 8150 // Insert start marker 8151 rng.collapse(); 8152 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>'); 8153 8154 // Insert end marker 8155 if (!collapsed) { 8156 rng2.collapse(false); 8157 8158 // Detect the empty space after block elements in IE and move the end back one character <p></p>] becomes <p>]</p> 8159 rng.moveToElementText(rng2.parentElement()); 8160 if (rng.compareEndPoints('StartToEnd', rng2) === 0) 8161 rng2.move('character', -1); 8162 8163 rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>'); 8164 } 8165 } catch (ex) { 8166 // IE might throw unspecified error so lets ignore it 8167 return null; 8168 } 8169 } else { 8170 // Control selection 8171 element = rng.item(0); 8172 name = element.nodeName; 8173 8174 return {name : name, index : findIndex(name, element)}; 8175 } 8176 } else { 8177 element = t.getNode(); 8178 name = element.nodeName; 8179 if (name == 'IMG') 8180 return {name : name, index : findIndex(name, element)}; 8181 8182 // W3C method 8183 rng2 = normalizeTableCellSelection(rng.cloneRange()); 8184 8185 // Insert end marker 8186 if (!collapsed) { 8187 rng2.collapse(false); 8188 rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr)); 8189 } 8190 8191 rng = normalizeTableCellSelection(rng); 8192 rng.collapse(true); 8193 rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr)); 8194 } 8195 8196 t.moveToBookmark({id : id, keep : 1}); 8197 8198 return {id : id}; 8199 }, 8200 8201 moveToBookmark : function(bookmark) { 8202 var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; 8203 8204 function setEndPoint(start) { 8205 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; 8206 8207 if (point) { 8208 offset = point[0]; 8209 8210 // Find container node 8211 for (node = root, i = point.length - 1; i >= 1; i--) { 8212 children = node.childNodes; 8213 8214 if (point[i] > children.length - 1) 8215 return; 8216 8217 node = children[point[i]]; 8218 } 8219 8220 // Move text offset to best suitable location 8221 if (node.nodeType === 3) 8222 offset = Math.min(point[0], node.nodeValue.length); 8223 8224 // Move element offset to best suitable location 8225 if (node.nodeType === 1) 8226 offset = Math.min(point[0], node.childNodes.length); 8227 8228 // Set offset within container node 8229 if (start) 8230 rng.setStart(node, offset); 8231 else 8232 rng.setEnd(node, offset); 8233 } 8234 8235 return true; 8236 }; 8237 8238 function restoreEndPoint(suffix) { 8239 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; 8240 8241 if (marker) { 8242 node = marker.parentNode; 8243 8244 if (suffix == 'start') { 8245 if (!keep) { 8246 idx = dom.nodeIndex(marker); 8247 } else { 8248 node = marker.firstChild; 8249 idx = 1; 8250 } 8251 8252 startContainer = endContainer = node; 8253 startOffset = endOffset = idx; 8254 } else { 8255 if (!keep) { 8256 idx = dom.nodeIndex(marker); 8257 } else { 8258 node = marker.firstChild; 8259 idx = 1; 8260 } 8261 8262 endContainer = node; 8263 endOffset = idx; 8264 } 8265 8266 if (!keep) { 8267 prev = marker.previousSibling; 8268 next = marker.nextSibling; 8269 8270 // Remove all marker text nodes 8271 each(tinymce.grep(marker.childNodes), function(node) { 8272 if (node.nodeType == 3) 8273 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); 8274 }); 8275 8276 // Remove marker but keep children if for example contents where inserted into the marker 8277 // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature 8278 while (marker = dom.get(bookmark.id + '_' + suffix)) 8279 dom.remove(marker, 1); 8280 8281 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node 8282 // and we are sniffing since adding a lot of detection code for a browser with 3% of the market isn't worth the effort. Sorry, Opera but it's just a fact 8283 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) { 8284 idx = prev.nodeValue.length; 8285 prev.appendData(next.nodeValue); 8286 dom.remove(next); 8287 8288 if (suffix == 'start') { 8289 startContainer = endContainer = prev; 8290 startOffset = endOffset = idx; 8291 } else { 8292 endContainer = prev; 8293 endOffset = idx; 8294 } 8295 } 8296 } 8297 } 8298 }; 8299 8300 function addBogus(node) { 8301 // Adds a bogus BR element for empty block elements 8302 if (dom.isBlock(node) && !node.innerHTML && !isIE) 8303 node.innerHTML = '<br data-mce-bogus="1" />'; 8304 8305 return node; 8306 }; 8307 8308 if (bookmark) { 8309 if (bookmark.start) { 8310 rng = dom.createRng(); 8311 root = dom.getRoot(); 8312 8313 if (t.tridentSel) 8314 return t.tridentSel.moveToBookmark(bookmark); 8315 8316 if (setEndPoint(true) && setEndPoint()) { 8317 t.setRng(rng); 8318 } 8319 } else if (bookmark.id) { 8320 // Restore start/end points 8321 restoreEndPoint('start'); 8322 restoreEndPoint('end'); 8323 8324 if (startContainer) { 8325 rng = dom.createRng(); 8326 rng.setStart(addBogus(startContainer), startOffset); 8327 rng.setEnd(addBogus(endContainer), endOffset); 8328 t.setRng(rng); 8329 } 8330 } else if (bookmark.name) { 8331 t.select(dom.select(bookmark.name)[bookmark.index]); 8332 } else if (bookmark.rng) 8333 t.setRng(bookmark.rng); 8334 } 8335 }, 8336 8337 select : function(node, content) { 8338 var t = this, dom = t.dom, rng = dom.createRng(), idx; 8339 8340 function setPoint(node, start) { 8341 var walker = new TreeWalker(node, node); 8342 8343 do { 8344 // Text node 8345 if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length !== 0) { 8346 if (start) 8347 rng.setStart(node, 0); 8348 else 8349 rng.setEnd(node, node.nodeValue.length); 8350 8351 return; 8352 } 8353 8354 // BR element 8355 if (node.nodeName == 'BR') { 8356 if (start) 8357 rng.setStartBefore(node); 8358 else 8359 rng.setEndBefore(node); 8360 8361 return; 8362 } 8363 } while (node = (start ? walker.next() : walker.prev())); 8364 }; 8365 8366 if (node) { 8367 idx = dom.nodeIndex(node); 8368 rng.setStart(node.parentNode, idx); 8369 rng.setEnd(node.parentNode, idx + 1); 8370 8371 // Find first/last text node or BR element 8372 if (content) { 8373 setPoint(node, 1); 8374 setPoint(node); 8375 } 8376 8377 t.setRng(rng); 8378 } 8379 8380 return node; 8381 }, 8382 8383 isCollapsed : function() { 8384 var t = this, r = t.getRng(), s = t.getSel(); 8385 8386 if (!r || r.item) 8387 return false; 8388 8389 if (r.compareEndPoints) 8390 return r.compareEndPoints('StartToEnd', r) === 0; 8391 8392 return !s || r.collapsed; 8393 }, 8394 8395 collapse : function(to_start) { 8396 var self = this, rng = self.getRng(), node; 8397 8398 // Control range on IE 8399 if (rng.item) { 8400 node = rng.item(0); 8401 rng = self.win.document.body.createTextRange(); 8402 rng.moveToElementText(node); 8403 } 8404 8405 rng.collapse(!!to_start); 8406 self.setRng(rng); 8407 }, 8408 8409 getSel : function() { 8410 var t = this, w = this.win; 8411 8412 return w.getSelection ? w.getSelection() : w.document.selection; 8413 }, 8414 8415 getRng : function(w3c) { 8416 var self = this, selection, rng, elm, doc = self.win.document; 8417 8418 // Found tridentSel object then we need to use that one 8419 if (w3c && self.tridentSel) { 8420 return self.tridentSel.getRangeAt(0); 8421 } 8422 8423 try { 8424 if (selection = self.getSel()) { 8425 rng = selection.rangeCount > 0 ? selection.getRangeAt(0) : (selection.createRange ? selection.createRange() : doc.createRange()); 8426 } 8427 } catch (ex) { 8428 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe 8429 } 8430 8431 // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet 8432 if (tinymce.isIE && rng && rng.setStart && doc.selection.createRange().item) { 8433 elm = doc.selection.createRange().item(0); 8434 rng = doc.createRange(); 8435 rng.setStartBefore(elm); 8436 rng.setEndAfter(elm); 8437 } 8438 8439 // No range found then create an empty one 8440 // This can occur when the editor is placed in a hidden container element on Gecko 8441 // Or on IE when there was an exception 8442 if (!rng) { 8443 rng = doc.createRange ? doc.createRange() : doc.body.createTextRange(); 8444 } 8445 8446 // If range is at start of document then move it to start of body 8447 if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) { 8448 elm = self.dom.getRoot(); 8449 rng.setStart(elm, 0); 8450 rng.setEnd(elm, 0); 8451 } 8452 8453 if (self.selectedRange && self.explicitRange) { 8454 if (rng.compareBoundaryPoints(rng.START_TO_START, self.selectedRange) === 0 && rng.compareBoundaryPoints(rng.END_TO_END, self.selectedRange) === 0) { 8455 // Safari, Opera and Chrome only ever select text which causes the range to change. 8456 // This lets us use the originally set range if the selection hasn't been changed by the user. 8457 rng = self.explicitRange; 8458 } else { 8459 self.selectedRange = null; 8460 self.explicitRange = null; 8461 } 8462 } 8463 8464 return rng; 8465 }, 8466 8467 setRng : function(r, forward) { 8468 var s, t = this; 8469 8470 if (!t.tridentSel) { 8471 s = t.getSel(); 8472 8473 if (s) { 8474 t.explicitRange = r; 8475 8476 try { 8477 s.removeAllRanges(); 8478 } catch (ex) { 8479 // IE9 might throw errors here don't know why 8480 } 8481 8482 s.addRange(r); 8483 8484 // Forward is set to false and we have an extend function 8485 if (forward === false && s.extend) { 8486 s.collapse(r.endContainer, r.endOffset); 8487 s.extend(r.startContainer, r.startOffset); 8488 } 8489 8490 // adding range isn't always successful so we need to check range count otherwise an exception can occur 8491 t.selectedRange = s.rangeCount > 0 ? s.getRangeAt(0) : null; 8492 } 8493 } else { 8494 // Is W3C Range 8495 if (r.cloneRange) { 8496 try { 8497 t.tridentSel.addRange(r); 8498 return; 8499 } catch (ex) { 8500 //IE9 throws an error here if called before selection is placed in the editor 8501 } 8502 } 8503 8504 // Is IE specific range 8505 try { 8506 r.select(); 8507 } catch (ex) { 8508 // Needed for some odd IE bug #1843306 8509 } 8510 } 8511 }, 8512 8513 setNode : function(n) { 8514 var t = this; 8515 8516 t.setContent(t.dom.getOuterHTML(n)); 8517 8518 return n; 8519 }, 8520 8521 getNode : function() { 8522 var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer; 8523 8524 function skipEmptyTextNodes(n, forwards) { 8525 var orig = n; 8526 while (n && n.nodeType === 3 && n.length === 0) { 8527 n = forwards ? n.nextSibling : n.previousSibling; 8528 } 8529 return n || orig; 8530 }; 8531 8532 // Range maybe lost after the editor is made visible again 8533 if (!rng) 8534 return t.dom.getRoot(); 8535 8536 if (rng.setStart) { 8537 elm = rng.commonAncestorContainer; 8538 8539 // Handle selection a image or other control like element such as anchors 8540 if (!rng.collapsed) { 8541 if (rng.startContainer == rng.endContainer) { 8542 if (rng.endOffset - rng.startOffset < 2) { 8543 if (rng.startContainer.hasChildNodes()) 8544 elm = rng.startContainer.childNodes[rng.startOffset]; 8545 } 8546 } 8547 8548 // If the anchor node is a element instead of a text node then return this element 8549 //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) 8550 // return sel.anchorNode.childNodes[sel.anchorOffset]; 8551 8552 // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. 8553 // This happens when you double click an underlined word in FireFox. 8554 if (start.nodeType === 3 && end.nodeType === 3) { 8555 if (start.length === rng.startOffset) { 8556 start = skipEmptyTextNodes(start.nextSibling, true); 8557 } else { 8558 start = start.parentNode; 8559 } 8560 if (rng.endOffset === 0) { 8561 end = skipEmptyTextNodes(end.previousSibling, false); 8562 } else { 8563 end = end.parentNode; 8564 } 8565 8566 if (start && start === end) 8567 return start; 8568 } 8569 } 8570 8571 if (elm && elm.nodeType == 3) 8572 return elm.parentNode; 8573 8574 return elm; 8575 } 8576 8577 return rng.item ? rng.item(0) : rng.parentElement(); 8578 }, 8579 8580 getSelectedBlocks : function(st, en) { 8581 var t = this, dom = t.dom, sb, eb, n, bl = []; 8582 8583 sb = dom.getParent(st || t.getStart(), dom.isBlock); 8584 eb = dom.getParent(en || t.getEnd(), dom.isBlock); 8585 8586 if (sb) 8587 bl.push(sb); 8588 8589 if (sb && eb && sb != eb) { 8590 n = sb; 8591 8592 var walker = new TreeWalker(sb, dom.getRoot()); 8593 while ((n = walker.next()) && n != eb) { 8594 if (dom.isBlock(n)) 8595 bl.push(n); 8596 } 8597 } 8598 8599 if (eb && sb != eb) 8600 bl.push(eb); 8601 8602 return bl; 8603 }, 8604 8605 isForward: function(){ 8606 var dom = this.dom, sel = this.getSel(), anchorRange, focusRange; 8607 8608 // No support for selection direction then always return true 8609 if (!sel || sel.anchorNode == null || sel.focusNode == null) { 8610 return true; 8611 } 8612 8613 anchorRange = dom.createRng(); 8614 anchorRange.setStart(sel.anchorNode, sel.anchorOffset); 8615 anchorRange.collapse(true); 8616 8617 focusRange = dom.createRng(); 8618 focusRange.setStart(sel.focusNode, sel.focusOffset); 8619 focusRange.collapse(true); 8620 8621 return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0; 8622 }, 8623 8624 normalize : function() { 8625 var self = this, rng, normalized, collapsed, node, sibling; 8626 8627 function normalizeEndPoint(start) { 8628 var container, offset, walker, dom = self.dom, body = dom.getRoot(), node, nonEmptyElementsMap, nodeName; 8629 8630 function hasBrBeforeAfter(node, left) { 8631 var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); 8632 8633 while (node = walker[left ? 'prev' : 'next']()) { 8634 if (node.nodeName === "BR") { 8635 return true; 8636 } 8637 } 8638 }; 8639 8640 // Walks the dom left/right to find a suitable text node to move the endpoint into 8641 // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG 8642 function findTextNodeRelative(left, startNode) { 8643 var walker, lastInlineElement; 8644 8645 startNode = startNode || container; 8646 walker = new TreeWalker(startNode, dom.getParent(startNode.parentNode, dom.isBlock) || body); 8647 8648 // Walk left until we hit a text node we can move to or a block/br/img 8649 while (node = walker[left ? 'prev' : 'next']()) { 8650 // Found text node that has a length 8651 if (node.nodeType === 3 && node.nodeValue.length > 0) { 8652 container = node; 8653 offset = left ? node.nodeValue.length : 0; 8654 normalized = true; 8655 return; 8656 } 8657 8658 // Break if we find a block or a BR/IMG/INPUT etc 8659 if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 8660 return; 8661 } 8662 8663 lastInlineElement = node; 8664 } 8665 8666 // Only fetch the last inline element when in caret mode for now 8667 if (collapsed && lastInlineElement) { 8668 container = lastInlineElement; 8669 normalized = true; 8670 offset = 0; 8671 } 8672 }; 8673 8674 container = rng[(start ? 'start' : 'end') + 'Container']; 8675 offset = rng[(start ? 'start' : 'end') + 'Offset']; 8676 nonEmptyElementsMap = dom.schema.getNonEmptyElements(); 8677 8678 // If the container is a document move it to the body element 8679 if (container.nodeType === 9) { 8680 container = dom.getRoot(); 8681 offset = 0; 8682 } 8683 8684 // If the container is body try move it into the closest text node or position 8685 if (container === body) { 8686 // If start is before/after a image, table etc 8687 if (start) { 8688 node = container.childNodes[offset > 0 ? offset - 1 : 0]; 8689 if (node) { 8690 nodeName = node.nodeName.toLowerCase(); 8691 if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") { 8692 return; 8693 } 8694 } 8695 } 8696 8697 // Resolve the index 8698 if (container.hasChildNodes()) { 8699 container = container.childNodes[Math.min(!start && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1)]; 8700 offset = 0; 8701 8702 // Don't walk into elements that doesn't have any child nodes like a IMG 8703 if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) { 8704 // Walk the DOM to find a text node to place the caret at or a BR 8705 node = container; 8706 walker = new TreeWalker(container, body); 8707 8708 do { 8709 // Found a text node use that position 8710 if (node.nodeType === 3 && node.nodeValue.length > 0) { 8711 offset = start ? 0 : node.nodeValue.length; 8712 container = node; 8713 normalized = true; 8714 break; 8715 } 8716 8717 // Found a BR/IMG element that we can place the caret before 8718 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 8719 offset = dom.nodeIndex(node); 8720 container = node.parentNode; 8721 8722 // Put caret after image when moving the end point 8723 if (node.nodeName == "IMG" && !start) { 8724 offset++; 8725 } 8726 8727 normalized = true; 8728 break; 8729 } 8730 } while (node = (start ? walker.next() : walker.prev())); 8731 } 8732 } 8733 } 8734 8735 // Lean the caret to the left if possible 8736 if (collapsed) { 8737 // So this: <b>x</b><i>|x</i> 8738 // Becomes: <b>x|</b><i>x</i> 8739 // Seems that only gecko has issues with this 8740 if (container.nodeType === 3 && offset === 0) { 8741 findTextNodeRelative(true); 8742 } 8743 8744 // Lean left into empty inline elements when the caret is before a BR 8745 // So this: <i><b></b><i>|<br></i> 8746 // Becomes: <i><b>|</b><i><br></i> 8747 // Seems that only gecko has issues with this 8748 if (container.nodeType === 1) { 8749 node = container.childNodes[offset]; 8750 if(node && node.nodeName === 'BR' && !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) { 8751 findTextNodeRelative(true, container.childNodes[offset]); 8752 } 8753 } 8754 } 8755 8756 // Lean the start of the selection right if possible 8757 // So this: x[<b>x]</b> 8758 // Becomes: x<b>[x]</b> 8759 if (start && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) { 8760 findTextNodeRelative(false); 8761 } 8762 8763 // Set endpoint if it was normalized 8764 if (normalized) 8765 rng['set' + (start ? 'Start' : 'End')](container, offset); 8766 }; 8767 8768 // Normalize only on non IE browsers for now 8769 if (tinymce.isIE) 8770 return; 8771 8772 rng = self.getRng(); 8773 collapsed = rng.collapsed; 8774 8775 // Normalize the end points 8776 normalizeEndPoint(true); 8777 8778 if (!collapsed) 8779 normalizeEndPoint(); 8780 8781 // Set the selection if it was normalized 8782 if (normalized) { 8783 // If it was collapsed then make sure it still is 8784 if (collapsed) { 8785 rng.collapse(true); 8786 } 8787 8788 //console.log(self.dom.dumpRng(rng)); 8789 self.setRng(rng, self.isForward()); 8790 } 8791 }, 8792 8793 selectorChanged: function(selector, callback) { 8794 var self = this, currentSelectors; 8795 8796 if (!self.selectorChangedData) { 8797 self.selectorChangedData = {}; 8798 currentSelectors = {}; 8799 8800 self.editor.onNodeChange.addToTop(function(ed, cm, node) { 8801 var dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {}; 8802 8803 // Check for new matching selectors 8804 each(self.selectorChangedData, function(callbacks, selector) { 8805 each(parents, function(node) { 8806 if (dom.is(node, selector)) { 8807 if (!currentSelectors[selector]) { 8808 // Execute callbacks 8809 each(callbacks, function(callback) { 8810 callback(true, {node: node, selector: selector, parents: parents}); 8811 }); 8812 8813 currentSelectors[selector] = callbacks; 8814 } 8815 8816 matchedSelectors[selector] = callbacks; 8817 return false; 8818 } 8819 }); 8820 }); 8821 8822 // Check if current selectors still match 8823 each(currentSelectors, function(callbacks, selector) { 8824 if (!matchedSelectors[selector]) { 8825 delete currentSelectors[selector]; 8826 8827 each(callbacks, function(callback) { 8828 callback(false, {node: node, selector: selector, parents: parents}); 8829 }); 8830 } 8831 }); 8832 }); 8833 } 8834 8835 // Add selector listeners 8836 if (!self.selectorChangedData[selector]) { 8837 self.selectorChangedData[selector] = []; 8838 } 8839 8840 self.selectorChangedData[selector].push(callback); 8841 8842 return self; 8843 }, 8844 8845 destroy : function(manual) { 8846 var self = this; 8847 8848 self.win = null; 8849 8850 // Manual destroy then remove unload handler 8851 if (!manual) 8852 tinymce.removeUnload(self.destroy); 8853 }, 8854 8855 // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode 8856 _fixIESelection : function() { 8857 var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm; 8858 8859 // Return range from point or null if it failed 8860 function rngFromPoint(x, y) { 8861 var rng = body.createTextRange(); 8862 8863 try { 8864 rng.moveToPoint(x, y); 8865 } catch (ex) { 8866 // IE sometimes throws and exception, so lets just ignore it 8867 rng = null; 8868 } 8869 8870 return rng; 8871 }; 8872 8873 // Fires while the selection is changing 8874 function selectionChange(e) { 8875 var pointRng; 8876 8877 // Check if the button is down or not 8878 if (e.button) { 8879 // Create range from mouse position 8880 pointRng = rngFromPoint(e.x, e.y); 8881 8882 if (pointRng) { 8883 // Check if pointRange is before/after selection then change the endPoint 8884 if (pointRng.compareEndPoints('StartToStart', startRng) > 0) 8885 pointRng.setEndPoint('StartToStart', startRng); 8886 else 8887 pointRng.setEndPoint('EndToEnd', startRng); 8888 8889 pointRng.select(); 8890 } 8891 } else 8892 endSelection(); 8893 } 8894 8895 // Removes listeners 8896 function endSelection() { 8897 var rng = doc.selection.createRange(); 8898 8899 // If the range is collapsed then use the last start range 8900 if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) 8901 startRng.select(); 8902 8903 dom.unbind(doc, 'mouseup', endSelection); 8904 dom.unbind(doc, 'mousemove', selectionChange); 8905 startRng = started = 0; 8906 }; 8907 8908 // Make HTML element unselectable since we are going to handle selection by hand 8909 doc.documentElement.unselectable = true; 8910 8911 // Detect when user selects outside BODY 8912 dom.bind(doc, ['mousedown', 'contextmenu'], function(e) { 8913 if (e.target.nodeName === 'HTML') { 8914 if (started) 8915 endSelection(); 8916 8917 // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML 8918 htmlElm = doc.documentElement; 8919 if (htmlElm.scrollHeight > htmlElm.clientHeight) 8920 return; 8921 8922 started = 1; 8923 // Setup start position 8924 startRng = rngFromPoint(e.x, e.y); 8925 if (startRng) { 8926 // Listen for selection change events 8927 dom.bind(doc, 'mouseup', endSelection); 8928 dom.bind(doc, 'mousemove', selectionChange); 8929 8930 dom.win.focus(); 8931 startRng.select(); 8932 } 8933 } 8934 }); 8935 } 8936 }); 8937 })(tinymce); 8938 8939 (function(tinymce) { 8940 tinymce.dom.Serializer = function(settings, dom, schema) { 8941 var onPreProcess, onPostProcess, isIE = tinymce.isIE, each = tinymce.each, htmlParser; 8942 8943 // Support the old apply_source_formatting option 8944 if (!settings.apply_source_formatting) 8945 settings.indent = false; 8946 8947 // Default DOM and Schema if they are undefined 8948 dom = dom || tinymce.DOM; 8949 schema = schema || new tinymce.html.Schema(settings); 8950 settings.entity_encoding = settings.entity_encoding || 'named'; 8951 settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; 8952 8953 onPreProcess = new tinymce.util.Dispatcher(self); 8954 8955 onPostProcess = new tinymce.util.Dispatcher(self); 8956 8957 htmlParser = new tinymce.html.DomParser(settings, schema); 8958 8959 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed 8960 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { 8961 var i = nodes.length, node, value, internalName = 'data-mce-' + name, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; 8962 8963 while (i--) { 8964 node = nodes[i]; 8965 8966 value = node.attributes.map[internalName]; 8967 if (value !== undef) { 8968 // Set external name to internal value and remove internal 8969 node.attr(name, value.length > 0 ? value : null); 8970 node.attr(internalName, null); 8971 } else { 8972 // No internal attribute found then convert the value we have in the DOM 8973 value = node.attributes.map[name]; 8974 8975 if (name === "style") 8976 value = dom.serializeStyle(dom.parseStyle(value), node.name); 8977 else if (urlConverter) 8978 value = urlConverter.call(urlConverterScope, value, name, node.name); 8979 8980 node.attr(name, value.length > 0 ? value : null); 8981 } 8982 } 8983 }); 8984 8985 // Remove internal classes mceItem<..> or mceSelected 8986 htmlParser.addAttributeFilter('class', function(nodes, name) { 8987 var i = nodes.length, node, value; 8988 8989 while (i--) { 8990 node = nodes[i]; 8991 value = node.attr('class').replace(/(?:^|\s)mce(Item\w+|Selected)(?!\S)/g, ''); 8992 node.attr('class', value.length > 0 ? value : null); 8993 } 8994 }); 8995 8996 // Remove bookmark elements 8997 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) { 8998 var i = nodes.length, node; 8999 9000 while (i--) { 9001 node = nodes[i]; 9002 9003 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) 9004 node.remove(); 9005 } 9006 }); 9007 9008 // Remove expando attributes 9009 htmlParser.addAttributeFilter('data-mce-expando', function(nodes, name, args) { 9010 var i = nodes.length; 9011 9012 while (i--) { 9013 nodes[i].attr(name, null); 9014 } 9015 }); 9016 9017 // Force script into CDATA sections and remove the mce- prefix also add comments around styles 9018 htmlParser.addNodeFilter('script,style', function(nodes, name) { 9019 var i = nodes.length, node, value; 9020 9021 function trim(value) { 9022 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n') 9023 .replace(/^[\r\n]*|[\r\n]*$/g, '') 9024 .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '') 9025 .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); 9026 }; 9027 9028 while (i--) { 9029 node = nodes[i]; 9030 value = node.firstChild ? node.firstChild.value : ''; 9031 9032 if (name === "script") { 9033 // Remove mce- prefix from script elements 9034 node.attr('type', (node.attr('type') || 'text/javascript').replace(/^mce\-/, '')); 9035 9036 if (value.length > 0) 9037 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>'; 9038 } else { 9039 if (value.length > 0) 9040 node.firstChild.value = '<!--\n' + trim(value) + '\n-->'; 9041 } 9042 } 9043 }); 9044 9045 // Convert comments to cdata and handle protected comments 9046 htmlParser.addNodeFilter('#comment', function(nodes, name) { 9047 var i = nodes.length, node; 9048 9049 while (i--) { 9050 node = nodes[i]; 9051 9052 if (node.value.indexOf('[CDATA[') === 0) { 9053 node.name = '#cdata'; 9054 node.type = 4; 9055 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); 9056 } else if (node.value.indexOf('mce:protected ') === 0) { 9057 node.name = "#text"; 9058 node.type = 3; 9059 node.raw = true; 9060 node.value = unescape(node.value).substr(14); 9061 } 9062 } 9063 }); 9064 9065 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) { 9066 var i = nodes.length, node; 9067 9068 while (i--) { 9069 node = nodes[i]; 9070 if (node.type === 7) 9071 node.remove(); 9072 else if (node.type === 1) { 9073 if (name === "input" && !("type" in node.attributes.map)) 9074 node.attr('type', 'text'); 9075 } 9076 } 9077 }); 9078 9079 // Fix list elements, TODO: Replace this later 9080 if (settings.fix_list_elements) { 9081 htmlParser.addNodeFilter('ul,ol', function(nodes, name) { 9082 var i = nodes.length, node, parentNode; 9083 9084 while (i--) { 9085 node = nodes[i]; 9086 parentNode = node.parent; 9087 9088 if (parentNode.name === 'ul' || parentNode.name === 'ol') { 9089 if (node.prev && node.prev.name === 'li') { 9090 node.prev.append(node); 9091 } 9092 } 9093 } 9094 }); 9095 } 9096 9097 // Remove internal data attributes 9098 htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', function(nodes, name) { 9099 var i = nodes.length; 9100 9101 while (i--) { 9102 nodes[i].attr(name, null); 9103 } 9104 }); 9105 9106 // Return public methods 9107 return { 9108 schema : schema, 9109 9110 addNodeFilter : htmlParser.addNodeFilter, 9111 9112 addAttributeFilter : htmlParser.addAttributeFilter, 9113 9114 onPreProcess : onPreProcess, 9115 9116 onPostProcess : onPostProcess, 9117 9118 serialize : function(node, args) { 9119 var impl, doc, oldDoc, htmlSerializer, content; 9120 9121 // Explorer won't clone contents of script and style and the 9122 // selected index of select elements are cleared on a clone operation. 9123 if (isIE && dom.select('script,style,select,map').length > 0) { 9124 content = node.innerHTML; 9125 node = node.cloneNode(false); 9126 dom.setHTML(node, content); 9127 } else 9128 node = node.cloneNode(true); 9129 9130 // Nodes needs to be attached to something in WebKit/Opera 9131 // Older builds of Opera crashes if you attach the node to an document created dynamically 9132 // and since we can't feature detect a crash we need to sniff the acutal build number 9133 // This fix will make DOM ranges and make Sizzle happy! 9134 impl = node.ownerDocument.implementation; 9135 if (impl.createHTMLDocument) { 9136 // Create an empty HTML document 9137 doc = impl.createHTMLDocument(""); 9138 9139 // Add the element or it's children if it's a body element to the new document 9140 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) { 9141 doc.body.appendChild(doc.importNode(node, true)); 9142 }); 9143 9144 // Grab first child or body element for serialization 9145 if (node.nodeName != 'BODY') 9146 node = doc.body.firstChild; 9147 else 9148 node = doc.body; 9149 9150 // set the new document in DOMUtils so createElement etc works 9151 oldDoc = dom.doc; 9152 dom.doc = doc; 9153 } 9154 9155 args = args || {}; 9156 args.format = args.format || 'html'; 9157 9158 // Pre process 9159 if (!args.no_events) { 9160 args.node = node; 9161 onPreProcess.dispatch(self, args); 9162 } 9163 9164 // Setup serializer 9165 htmlSerializer = new tinymce.html.Serializer(settings, schema); 9166 9167 // Parse and serialize HTML 9168 args.content = htmlSerializer.serialize( 9169 htmlParser.parse(tinymce.trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args) 9170 ); 9171 9172 // Replace all BOM characters for now until we can find a better solution 9173 if (!args.cleanup) 9174 args.content = args.content.replace(/\uFEFF|\u200B/g, ''); 9175 9176 // Post process 9177 if (!args.no_events) 9178 onPostProcess.dispatch(self, args); 9179 9180 // Restore the old document if it was changed 9181 if (oldDoc) 9182 dom.doc = oldDoc; 9183 9184 args.node = null; 9185 9186 return args.content; 9187 }, 9188 9189 addRules : function(rules) { 9190 schema.addValidElements(rules); 9191 }, 9192 9193 setRules : function(rules) { 9194 schema.setValidElements(rules); 9195 } 9196 }; 9197 }; 9198 })(tinymce); 9199 (function(tinymce) { 9200 tinymce.dom.ScriptLoader = function(settings) { 9201 var QUEUED = 0, 9202 LOADING = 1, 9203 LOADED = 2, 9204 states = {}, 9205 queue = [], 9206 scriptLoadedCallbacks = {}, 9207 queueLoadedCallbacks = [], 9208 loading = 0, 9209 undef; 9210 9211 function loadScript(url, callback) { 9212 var t = this, dom = tinymce.DOM, elm, uri, loc, id; 9213 9214 // Execute callback when script is loaded 9215 function done() { 9216 dom.remove(id); 9217 9218 if (elm) 9219 elm.onreadystatechange = elm.onload = elm = null; 9220 9221 callback(); 9222 }; 9223 9224 function error() { 9225 // Report the error so it's easier for people to spot loading errors 9226 if (typeof(console) !== "undefined" && console.log) 9227 console.log("Failed to load: " + url); 9228 9229 // We can't mark it as done if there is a load error since 9230 // A) We don't want to produce 404 errors on the server and 9231 // B) the onerror event won't fire on all browsers. 9232 // done(); 9233 }; 9234 9235 id = dom.uniqueId(); 9236 9237 if (tinymce.isIE6) { 9238 uri = new tinymce.util.URI(url); 9239 loc = location; 9240 9241 // If script is from same domain and we 9242 // use IE 6 then use XHR since it's more reliable 9243 if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol && uri.protocol.toLowerCase() != 'file') { 9244 tinymce.util.XHR.send({ 9245 url : tinymce._addVer(uri.getURI()), 9246 success : function(content) { 9247 // Create new temp script element 9248 var script = dom.create('script', { 9249 type : 'text/javascript' 9250 }); 9251 9252 // Evaluate script in global scope 9253 script.text = content; 9254 document.getElementsByTagName('head')[0].appendChild(script); 9255 dom.remove(script); 9256 9257 done(); 9258 }, 9259 9260 error : error 9261 }); 9262 9263 return; 9264 } 9265 } 9266 9267 // Create new script element 9268 elm = document.createElement('script'); 9269 elm.id = id; 9270 elm.type = 'text/javascript'; 9271 elm.src = tinymce._addVer(url); 9272 9273 // Add onload listener for non IE browsers since IE9 9274 // fires onload event before the script is parsed and executed 9275 if (!tinymce.isIE) 9276 elm.onload = done; 9277 9278 // Add onerror event will get fired on some browsers but not all of them 9279 elm.onerror = error; 9280 9281 // Opera 9.60 doesn't seem to fire the onreadystate event at correctly 9282 if (!tinymce.isOpera) { 9283 elm.onreadystatechange = function() { 9284 var state = elm.readyState; 9285 9286 // Loaded state is passed on IE 6 however there 9287 // are known issues with this method but we can't use 9288 // XHR in a cross domain loading 9289 if (state == 'complete' || state == 'loaded') 9290 done(); 9291 }; 9292 } 9293 9294 // Most browsers support this feature so we report errors 9295 // for those at least to help users track their missing plugins etc 9296 // todo: Removed since it produced error if the document is unloaded by navigating away, re-add it as an option 9297 /*elm.onerror = function() { 9298 alert('Failed to load: ' + url); 9299 };*/ 9300 9301 // Add script to document 9302 (document.getElementsByTagName('head')[0] || document.body).appendChild(elm); 9303 }; 9304 9305 this.isDone = function(url) { 9306 return states[url] == LOADED; 9307 }; 9308 9309 this.markDone = function(url) { 9310 states[url] = LOADED; 9311 }; 9312 9313 this.add = this.load = function(url, callback, scope) { 9314 var item, state = states[url]; 9315 9316 // Add url to load queue 9317 if (state == undef) { 9318 queue.push(url); 9319 states[url] = QUEUED; 9320 } 9321 9322 if (callback) { 9323 // Store away callback for later execution 9324 if (!scriptLoadedCallbacks[url]) 9325 scriptLoadedCallbacks[url] = []; 9326 9327 scriptLoadedCallbacks[url].push({ 9328 func : callback, 9329 scope : scope || this 9330 }); 9331 } 9332 }; 9333 9334 this.loadQueue = function(callback, scope) { 9335 this.loadScripts(queue, callback, scope); 9336 }; 9337 9338 this.loadScripts = function(scripts, callback, scope) { 9339 var loadScripts; 9340 9341 function execScriptLoadedCallbacks(url) { 9342 // Execute URL callback functions 9343 tinymce.each(scriptLoadedCallbacks[url], function(callback) { 9344 callback.func.call(callback.scope); 9345 }); 9346 9347 scriptLoadedCallbacks[url] = undef; 9348 }; 9349 9350 queueLoadedCallbacks.push({ 9351 func : callback, 9352 scope : scope || this 9353 }); 9354 9355 loadScripts = function() { 9356 var loadingScripts = tinymce.grep(scripts); 9357 9358 // Current scripts has been handled 9359 scripts.length = 0; 9360 9361 // Load scripts that needs to be loaded 9362 tinymce.each(loadingScripts, function(url) { 9363 // Script is already loaded then execute script callbacks directly 9364 if (states[url] == LOADED) { 9365 execScriptLoadedCallbacks(url); 9366 return; 9367 } 9368 9369 // Is script not loading then start loading it 9370 if (states[url] != LOADING) { 9371 states[url] = LOADING; 9372 loading++; 9373 9374 loadScript(url, function() { 9375 states[url] = LOADED; 9376 loading--; 9377 9378 execScriptLoadedCallbacks(url); 9379 9380 // Load more scripts if they where added by the recently loaded script 9381 loadScripts(); 9382 }); 9383 } 9384 }); 9385 9386 // No scripts are currently loading then execute all pending queue loaded callbacks 9387 if (!loading) { 9388 tinymce.each(queueLoadedCallbacks, function(callback) { 9389 callback.func.call(callback.scope); 9390 }); 9391 9392 queueLoadedCallbacks.length = 0; 9393 } 9394 }; 9395 9396 loadScripts(); 9397 }; 9398 }; 9399 9400 // Global script loader 9401 tinymce.ScriptLoader = new tinymce.dom.ScriptLoader(); 9402 })(tinymce); 9403 9404 (function(tinymce) { 9405 tinymce.dom.RangeUtils = function(dom) { 9406 var INVISIBLE_CHAR = '\uFEFF'; 9407 9408 this.walk = function(rng, callback) { 9409 var startContainer = rng.startContainer, 9410 startOffset = rng.startOffset, 9411 endContainer = rng.endContainer, 9412 endOffset = rng.endOffset, 9413 ancestor, startPoint, 9414 endPoint, node, parent, siblings, nodes; 9415 9416 // Handle table cell selection the table plugin enables 9417 // you to fake select table cells and perform formatting actions on them 9418 nodes = dom.select('td.mceSelected,th.mceSelected'); 9419 if (nodes.length > 0) { 9420 tinymce.each(nodes, function(node) { 9421 callback([node]); 9422 }); 9423 9424 return; 9425 } 9426 9427 function exclude(nodes) { 9428 var node; 9429 9430 // First node is excluded 9431 node = nodes[0]; 9432 if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { 9433 nodes.splice(0, 1); 9434 } 9435 9436 // Last node is excluded 9437 node = nodes[nodes.length - 1]; 9438 if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { 9439 nodes.splice(nodes.length - 1, 1); 9440 } 9441 9442 return nodes; 9443 }; 9444 9445 function collectSiblings(node, name, end_node) { 9446 var siblings = []; 9447 9448 for (; node && node != end_node; node = node[name]) 9449 siblings.push(node); 9450 9451 return siblings; 9452 }; 9453 9454 function findEndPoint(node, root) { 9455 do { 9456 if (node.parentNode == root) 9457 return node; 9458 9459 node = node.parentNode; 9460 } while(node); 9461 }; 9462 9463 function walkBoundary(start_node, end_node, next) { 9464 var siblingName = next ? 'nextSibling' : 'previousSibling'; 9465 9466 for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) { 9467 parent = node.parentNode; 9468 siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName); 9469 9470 if (siblings.length) { 9471 if (!next) 9472 siblings.reverse(); 9473 9474 callback(exclude(siblings)); 9475 } 9476 } 9477 }; 9478 9479 // If index based start position then resolve it 9480 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) 9481 startContainer = startContainer.childNodes[startOffset]; 9482 9483 // If index based end position then resolve it 9484 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) 9485 endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)]; 9486 9487 // Same container 9488 if (startContainer == endContainer) 9489 return callback(exclude([startContainer])); 9490 9491 // Find common ancestor and end points 9492 ancestor = dom.findCommonAncestor(startContainer, endContainer); 9493 9494 // Process left side 9495 for (node = startContainer; node; node = node.parentNode) { 9496 if (node === endContainer) 9497 return walkBoundary(startContainer, ancestor, true); 9498 9499 if (node === ancestor) 9500 break; 9501 } 9502 9503 // Process right side 9504 for (node = endContainer; node; node = node.parentNode) { 9505 if (node === startContainer) 9506 return walkBoundary(endContainer, ancestor); 9507 9508 if (node === ancestor) 9509 break; 9510 } 9511 9512 // Find start/end point 9513 startPoint = findEndPoint(startContainer, ancestor) || startContainer; 9514 endPoint = findEndPoint(endContainer, ancestor) || endContainer; 9515 9516 // Walk left leaf 9517 walkBoundary(startContainer, startPoint, true); 9518 9519 // Walk the middle from start to end point 9520 siblings = collectSiblings( 9521 startPoint == startContainer ? startPoint : startPoint.nextSibling, 9522 'nextSibling', 9523 endPoint == endContainer ? endPoint.nextSibling : endPoint 9524 ); 9525 9526 if (siblings.length) 9527 callback(exclude(siblings)); 9528 9529 // Walk right leaf 9530 walkBoundary(endContainer, endPoint); 9531 }; 9532 9533 this.split = function(rng) { 9534 var startContainer = rng.startContainer, 9535 startOffset = rng.startOffset, 9536 endContainer = rng.endContainer, 9537 endOffset = rng.endOffset; 9538 9539 function splitText(node, offset) { 9540 return node.splitText(offset); 9541 }; 9542 9543 // Handle single text node 9544 if (startContainer == endContainer && startContainer.nodeType == 3) { 9545 if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { 9546 endContainer = splitText(startContainer, startOffset); 9547 startContainer = endContainer.previousSibling; 9548 9549 if (endOffset > startOffset) { 9550 endOffset = endOffset - startOffset; 9551 startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; 9552 endOffset = endContainer.nodeValue.length; 9553 startOffset = 0; 9554 } else { 9555 endOffset = 0; 9556 } 9557 } 9558 } else { 9559 // Split startContainer text node if needed 9560 if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { 9561 startContainer = splitText(startContainer, startOffset); 9562 startOffset = 0; 9563 } 9564 9565 // Split endContainer text node if needed 9566 if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { 9567 endContainer = splitText(endContainer, endOffset).previousSibling; 9568 endOffset = endContainer.nodeValue.length; 9569 } 9570 } 9571 9572 return { 9573 startContainer : startContainer, 9574 startOffset : startOffset, 9575 endContainer : endContainer, 9576 endOffset : endOffset 9577 }; 9578 }; 9579 9580 }; 9581 9582 tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) { 9583 if (rng1 && rng2) { 9584 // Compare native IE ranges 9585 if (rng1.item || rng1.duplicate) { 9586 // Both are control ranges and the selected element matches 9587 if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) 9588 return true; 9589 9590 // Both are text ranges and the range matches 9591 if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) 9592 return true; 9593 } else { 9594 // Compare w3c ranges 9595 return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset; 9596 } 9597 } 9598 9599 return false; 9600 }; 9601 })(tinymce); 9602 9603 (function(tinymce) { 9604 var Event = tinymce.dom.Event, each = tinymce.each; 9605 9606 tinymce.create('tinymce.ui.KeyboardNavigation', { 9607 KeyboardNavigation: function(settings, dom) { 9608 var t = this, root = settings.root, items = settings.items, 9609 enableUpDown = settings.enableUpDown, enableLeftRight = settings.enableLeftRight || !settings.enableUpDown, 9610 excludeFromTabOrder = settings.excludeFromTabOrder, 9611 itemFocussed, itemBlurred, rootKeydown, rootFocussed, focussedId; 9612 9613 dom = dom || tinymce.DOM; 9614 9615 itemFocussed = function(evt) { 9616 focussedId = evt.target.id; 9617 }; 9618 9619 itemBlurred = function(evt) { 9620 dom.setAttrib(evt.target.id, 'tabindex', '-1'); 9621 }; 9622 9623 rootFocussed = function(evt) { 9624 var item = dom.get(focussedId); 9625 dom.setAttrib(item, 'tabindex', '0'); 9626 item.focus(); 9627 }; 9628 9629 t.focus = function() { 9630 dom.get(focussedId).focus(); 9631 }; 9632 9633 t.destroy = function() { 9634 each(items, function(item) { 9635 var elm = dom.get(item.id); 9636 9637 dom.unbind(elm, 'focus', itemFocussed); 9638 dom.unbind(elm, 'blur', itemBlurred); 9639 }); 9640 9641 var rootElm = dom.get(root); 9642 dom.unbind(rootElm, 'focus', rootFocussed); 9643 dom.unbind(rootElm, 'keydown', rootKeydown); 9644 9645 items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null; 9646 t.destroy = function() {}; 9647 }; 9648 9649 t.moveFocus = function(dir, evt) { 9650 var idx = -1, controls = t.controls, newFocus; 9651 9652 if (!focussedId) 9653 return; 9654 9655 each(items, function(item, index) { 9656 if (item.id === focussedId) { 9657 idx = index; 9658 return false; 9659 } 9660 }); 9661 9662 idx += dir; 9663 if (idx < 0) { 9664 idx = items.length - 1; 9665 } else if (idx >= items.length) { 9666 idx = 0; 9667 } 9668 9669 newFocus = items[idx]; 9670 dom.setAttrib(focussedId, 'tabindex', '-1'); 9671 dom.setAttrib(newFocus.id, 'tabindex', '0'); 9672 dom.get(newFocus.id).focus(); 9673 9674 if (settings.actOnFocus) { 9675 settings.onAction(newFocus.id); 9676 } 9677 9678 if (evt) 9679 Event.cancel(evt); 9680 }; 9681 9682 rootKeydown = function(evt) { 9683 var DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40, DOM_VK_ESCAPE = 27, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32; 9684 9685 switch (evt.keyCode) { 9686 case DOM_VK_LEFT: 9687 if (enableLeftRight) t.moveFocus(-1); 9688 break; 9689 9690 case DOM_VK_RIGHT: 9691 if (enableLeftRight) t.moveFocus(1); 9692 break; 9693 9694 case DOM_VK_UP: 9695 if (enableUpDown) t.moveFocus(-1); 9696 break; 9697 9698 case DOM_VK_DOWN: 9699 if (enableUpDown) t.moveFocus(1); 9700 break; 9701 9702 case DOM_VK_ESCAPE: 9703 if (settings.onCancel) { 9704 settings.onCancel(); 9705 Event.cancel(evt); 9706 } 9707 break; 9708 9709 case DOM_VK_ENTER: 9710 case DOM_VK_RETURN: 9711 case DOM_VK_SPACE: 9712 if (settings.onAction) { 9713 settings.onAction(focussedId); 9714 Event.cancel(evt); 9715 } 9716 break; 9717 } 9718 }; 9719 9720 // Set up state and listeners for each item. 9721 each(items, function(item, idx) { 9722 var tabindex, elm; 9723 9724 if (!item.id) { 9725 item.id = dom.uniqueId('_mce_item_'); 9726 } 9727 9728 elm = dom.get(item.id); 9729 9730 if (excludeFromTabOrder) { 9731 dom.bind(elm, 'blur', itemBlurred); 9732 tabindex = '-1'; 9733 } else { 9734 tabindex = (idx === 0 ? '0' : '-1'); 9735 } 9736 9737 elm.setAttribute('tabindex', tabindex); 9738 dom.bind(elm, 'focus', itemFocussed); 9739 }); 9740 9741 // Setup initial state for root element. 9742 if (items[0]){ 9743 focussedId = items[0].id; 9744 } 9745 9746 dom.setAttrib(root, 'tabindex', '-1'); 9747 9748 // Setup listeners for root element. 9749 var rootElm = dom.get(root); 9750 dom.bind(rootElm, 'focus', rootFocussed); 9751 dom.bind(rootElm, 'keydown', rootKeydown); 9752 } 9753 }); 9754 })(tinymce); 9755 9756 (function(tinymce) { 9757 // Shorten class names 9758 var DOM = tinymce.DOM, is = tinymce.is; 9759 9760 tinymce.create('tinymce.ui.Control', { 9761 Control : function(id, s, editor) { 9762 this.id = id; 9763 this.settings = s = s || {}; 9764 this.rendered = false; 9765 this.onRender = new tinymce.util.Dispatcher(this); 9766 this.classPrefix = ''; 9767 this.scope = s.scope || this; 9768 this.disabled = 0; 9769 this.active = 0; 9770 this.editor = editor; 9771 }, 9772 9773 setAriaProperty : function(property, value) { 9774 var element = DOM.get(this.id + '_aria') || DOM.get(this.id); 9775 if (element) { 9776 DOM.setAttrib(element, 'aria-' + property, !!value); 9777 } 9778 }, 9779 9780 focus : function() { 9781 DOM.get(this.id).focus(); 9782 }, 9783 9784 setDisabled : function(s) { 9785 if (s != this.disabled) { 9786 this.setAriaProperty('disabled', s); 9787 9788 this.setState('Disabled', s); 9789 this.setState('Enabled', !s); 9790 this.disabled = s; 9791 } 9792 }, 9793 9794 isDisabled : function() { 9795 return this.disabled; 9796 }, 9797 9798 setActive : function(s) { 9799 if (s != this.active) { 9800 this.setState('Active', s); 9801 this.active = s; 9802 this.setAriaProperty('pressed', s); 9803 } 9804 }, 9805 9806 isActive : function() { 9807 return this.active; 9808 }, 9809 9810 setState : function(c, s) { 9811 var n = DOM.get(this.id); 9812 9813 c = this.classPrefix + c; 9814 9815 if (s) 9816 DOM.addClass(n, c); 9817 else 9818 DOM.removeClass(n, c); 9819 }, 9820 9821 isRendered : function() { 9822 return this.rendered; 9823 }, 9824 9825 renderHTML : function() { 9826 }, 9827 9828 renderTo : function(n) { 9829 DOM.setHTML(n, this.renderHTML()); 9830 }, 9831 9832 postRender : function() { 9833 var t = this, b; 9834 9835 // Set pending states 9836 if (is(t.disabled)) { 9837 b = t.disabled; 9838 t.disabled = -1; 9839 t.setDisabled(b); 9840 } 9841 9842 if (is(t.active)) { 9843 b = t.active; 9844 t.active = -1; 9845 t.setActive(b); 9846 } 9847 }, 9848 9849 remove : function() { 9850 DOM.remove(this.id); 9851 this.destroy(); 9852 }, 9853 9854 destroy : function() { 9855 tinymce.dom.Event.clear(this.id); 9856 } 9857 }); 9858 })(tinymce); 9859 tinymce.create('tinymce.ui.Container:tinymce.ui.Control', { 9860 Container : function(id, s, editor) { 9861 this.parent(id, s, editor); 9862 9863 this.controls = []; 9864 9865 this.lookup = {}; 9866 }, 9867 9868 add : function(c) { 9869 this.lookup[c.id] = c; 9870 this.controls.push(c); 9871 9872 return c; 9873 }, 9874 9875 get : function(n) { 9876 return this.lookup[n]; 9877 } 9878 }); 9879 9880 9881 tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { 9882 Separator : function(id, s) { 9883 this.parent(id, s); 9884 this.classPrefix = 'mceSeparator'; 9885 this.setDisabled(true); 9886 }, 9887 9888 renderHTML : function() { 9889 return tinymce.DOM.createHTML('span', {'class' : this.classPrefix, role : 'separator', 'aria-orientation' : 'vertical', tabindex : '-1'}); 9890 } 9891 }); 9892 9893 (function(tinymce) { 9894 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk; 9895 9896 tinymce.create('tinymce.ui.MenuItem:tinymce.ui.Control', { 9897 MenuItem : function(id, s) { 9898 this.parent(id, s); 9899 this.classPrefix = 'mceMenuItem'; 9900 }, 9901 9902 setSelected : function(s) { 9903 this.setState('Selected', s); 9904 this.setAriaProperty('checked', !!s); 9905 this.selected = s; 9906 }, 9907 9908 isSelected : function() { 9909 return this.selected; 9910 }, 9911 9912 postRender : function() { 9913 var t = this; 9914 9915 t.parent(); 9916 9917 // Set pending state 9918 if (is(t.selected)) 9919 t.setSelected(t.selected); 9920 } 9921 }); 9922 })(tinymce); 9923 9924 (function(tinymce) { 9925 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk; 9926 9927 tinymce.create('tinymce.ui.Menu:tinymce.ui.MenuItem', { 9928 Menu : function(id, s) { 9929 var t = this; 9930 9931 t.parent(id, s); 9932 t.items = {}; 9933 t.collapsed = false; 9934 t.menuCount = 0; 9935 t.onAddItem = new tinymce.util.Dispatcher(this); 9936 }, 9937 9938 expand : function(d) { 9939 var t = this; 9940 9941 if (d) { 9942 walk(t, function(o) { 9943 if (o.expand) 9944 o.expand(); 9945 }, 'items', t); 9946 } 9947 9948 t.collapsed = false; 9949 }, 9950 9951 collapse : function(d) { 9952 var t = this; 9953 9954 if (d) { 9955 walk(t, function(o) { 9956 if (o.collapse) 9957 o.collapse(); 9958 }, 'items', t); 9959 } 9960 9961 t.collapsed = true; 9962 }, 9963 9964 isCollapsed : function() { 9965 return this.collapsed; 9966 }, 9967 9968 add : function(o) { 9969 if (!o.settings) 9970 o = new tinymce.ui.MenuItem(o.id || DOM.uniqueId(), o); 9971 9972 this.onAddItem.dispatch(this, o); 9973 9974 return this.items[o.id] = o; 9975 }, 9976 9977 addSeparator : function() { 9978 return this.add({separator : true}); 9979 }, 9980 9981 addMenu : function(o) { 9982 if (!o.collapse) 9983 o = this.createMenu(o); 9984 9985 this.menuCount++; 9986 9987 return this.add(o); 9988 }, 9989 9990 hasMenus : function() { 9991 return this.menuCount !== 0; 9992 }, 9993 9994 remove : function(o) { 9995 delete this.items[o.id]; 9996 }, 9997 9998 removeAll : function() { 9999 var t = this; 10000 10001 walk(t, function(o) { 10002 if (o.removeAll) 10003 o.removeAll(); 10004 else 10005 o.remove(); 10006 10007 o.destroy(); 10008 }, 'items', t); 10009 10010 t.items = {}; 10011 }, 10012 10013 createMenu : function(o) { 10014 var m = new tinymce.ui.Menu(o.id || DOM.uniqueId(), o); 10015 10016 m.onAddItem.add(this.onAddItem.dispatch, this.onAddItem); 10017 10018 return m; 10019 } 10020 }); 10021 })(tinymce); 10022 (function(tinymce) { 10023 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event, Element = tinymce.dom.Element; 10024 10025 tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', { 10026 DropMenu : function(id, s) { 10027 s = s || {}; 10028 s.container = s.container || DOM.doc.body; 10029 s.offset_x = s.offset_x || 0; 10030 s.offset_y = s.offset_y || 0; 10031 s.vp_offset_x = s.vp_offset_x || 0; 10032 s.vp_offset_y = s.vp_offset_y || 0; 10033 10034 if (is(s.icons) && !s.icons) 10035 s['class'] += ' mceNoIcons'; 10036 10037 this.parent(id, s); 10038 this.onShowMenu = new tinymce.util.Dispatcher(this); 10039 this.onHideMenu = new tinymce.util.Dispatcher(this); 10040 this.classPrefix = 'mceMenu'; 10041 }, 10042 10043 createMenu : function(s) { 10044 var t = this, cs = t.settings, m; 10045 10046 s.container = s.container || cs.container; 10047 s.parent = t; 10048 s.constrain = s.constrain || cs.constrain; 10049 s['class'] = s['class'] || cs['class']; 10050 s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x; 10051 s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y; 10052 s.keyboard_focus = cs.keyboard_focus; 10053 m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s); 10054 10055 m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem); 10056 10057 return m; 10058 }, 10059 10060 focus : function() { 10061 var t = this; 10062 if (t.keyboardNav) { 10063 t.keyboardNav.focus(); 10064 } 10065 }, 10066 10067 update : function() { 10068 var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th; 10069 10070 tw = s.max_width ? Math.min(tb.offsetWidth, s.max_width) : tb.offsetWidth; 10071 th = s.max_height ? Math.min(tb.offsetHeight, s.max_height) : tb.offsetHeight; 10072 10073 if (!DOM.boxModel) 10074 t.element.setStyles({width : tw + 2, height : th + 2}); 10075 else 10076 t.element.setStyles({width : tw, height : th}); 10077 10078 if (s.max_width) 10079 DOM.setStyle(co, 'width', tw); 10080 10081 if (s.max_height) { 10082 DOM.setStyle(co, 'height', th); 10083 10084 if (tb.clientHeight < s.max_height) 10085 DOM.setStyle(co, 'overflow', 'hidden'); 10086 } 10087 }, 10088 10089 showMenu : function(x, y, px) { 10090 var t = this, s = t.settings, co, vp = DOM.getViewPort(), w, h, mx, my, ot = 2, dm, tb, cp = t.classPrefix; 10091 10092 t.collapse(1); 10093 10094 if (t.isMenuVisible) 10095 return; 10096 10097 if (!t.rendered) { 10098 co = DOM.add(t.settings.container, t.renderNode()); 10099 10100 each(t.items, function(o) { 10101 o.postRender(); 10102 }); 10103 10104 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container}); 10105 } else 10106 co = DOM.get('menu_' + t.id); 10107 10108 // Move layer out of sight unless it's Opera since it scrolls to top of page due to an bug 10109 if (!tinymce.isOpera) 10110 DOM.setStyles(co, {left : -0xFFFF , top : -0xFFFF}); 10111 10112 DOM.show(co); 10113 t.update(); 10114 10115 x += s.offset_x || 0; 10116 y += s.offset_y || 0; 10117 vp.w -= 4; 10118 vp.h -= 4; 10119 10120 // Move inside viewport if not submenu 10121 if (s.constrain) { 10122 w = co.clientWidth - ot; 10123 h = co.clientHeight - ot; 10124 mx = vp.x + vp.w; 10125 my = vp.y + vp.h; 10126 10127 if ((x + s.vp_offset_x + w) > mx) 10128 x = px ? px - w : Math.max(0, (mx - s.vp_offset_x) - w); 10129 10130 if ((y + s.vp_offset_y + h) > my) 10131 y = Math.max(0, (my - s.vp_offset_y) - h); 10132 } 10133 10134 DOM.setStyles(co, {left : x , top : y}); 10135 t.element.update(); 10136 10137 t.isMenuVisible = 1; 10138 t.mouseClickFunc = Event.add(co, 'click', function(e) { 10139 var m; 10140 10141 e = e.target; 10142 10143 if (e && (e = DOM.getParent(e, 'tr')) && !DOM.hasClass(e, cp + 'ItemSub')) { 10144 m = t.items[e.id]; 10145 10146 if (m.isDisabled()) 10147 return; 10148 10149 dm = t; 10150 10151 while (dm) { 10152 if (dm.hideMenu) 10153 dm.hideMenu(); 10154 10155 dm = dm.settings.parent; 10156 } 10157 10158 if (m.settings.onclick) 10159 m.settings.onclick(e); 10160 10161 return false; // Cancel to fix onbeforeunload problem 10162 } 10163 }); 10164 10165 if (t.hasMenus()) { 10166 t.mouseOverFunc = Event.add(co, 'mouseover', function(e) { 10167 var m, r, mi; 10168 10169 e = e.target; 10170 if (e && (e = DOM.getParent(e, 'tr'))) { 10171 m = t.items[e.id]; 10172 10173 if (t.lastMenu) 10174 t.lastMenu.collapse(1); 10175 10176 if (m.isDisabled()) 10177 return; 10178 10179 if (e && DOM.hasClass(e, cp + 'ItemSub')) { 10180 //p = DOM.getPos(s.container); 10181 r = DOM.getRect(e); 10182 m.showMenu((r.x + r.w - ot), r.y - ot, r.x); 10183 t.lastMenu = m; 10184 DOM.addClass(DOM.get(m.id).firstChild, cp + 'ItemActive'); 10185 } 10186 } 10187 }); 10188 } 10189 10190 Event.add(co, 'keydown', t._keyHandler, t); 10191 10192 t.onShowMenu.dispatch(t); 10193 10194 if (s.keyboard_focus) { 10195 t._setupKeyboardNav(); 10196 } 10197 }, 10198 10199 hideMenu : function(c) { 10200 var t = this, co = DOM.get('menu_' + t.id), e; 10201 10202 if (!t.isMenuVisible) 10203 return; 10204 10205 if (t.keyboardNav) t.keyboardNav.destroy(); 10206 Event.remove(co, 'mouseover', t.mouseOverFunc); 10207 Event.remove(co, 'click', t.mouseClickFunc); 10208 Event.remove(co, 'keydown', t._keyHandler); 10209 DOM.hide(co); 10210 t.isMenuVisible = 0; 10211 10212 if (!c) 10213 t.collapse(1); 10214 10215 if (t.element) 10216 t.element.hide(); 10217 10218 if (e = DOM.get(t.id)) 10219 DOM.removeClass(e.firstChild, t.classPrefix + 'ItemActive'); 10220 10221 t.onHideMenu.dispatch(t); 10222 }, 10223 10224 add : function(o) { 10225 var t = this, co; 10226 10227 o = t.parent(o); 10228 10229 if (t.isRendered && (co = DOM.get('menu_' + t.id))) 10230 t._add(DOM.select('tbody', co)[0], o); 10231 10232 return o; 10233 }, 10234 10235 collapse : function(d) { 10236 this.parent(d); 10237 this.hideMenu(1); 10238 }, 10239 10240 remove : function(o) { 10241 DOM.remove(o.id); 10242 this.destroy(); 10243 10244 return this.parent(o); 10245 }, 10246 10247 destroy : function() { 10248 var t = this, co = DOM.get('menu_' + t.id); 10249 10250 if (t.keyboardNav) t.keyboardNav.destroy(); 10251 Event.remove(co, 'mouseover', t.mouseOverFunc); 10252 Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc); 10253 Event.remove(co, 'click', t.mouseClickFunc); 10254 Event.remove(co, 'keydown', t._keyHandler); 10255 10256 if (t.element) 10257 t.element.remove(); 10258 10259 DOM.remove(co); 10260 }, 10261 10262 renderNode : function() { 10263 var t = this, s = t.settings, n, tb, co, w; 10264 10265 w = DOM.create('div', {role: 'listbox', id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000;outline:0'}); 10266 if (t.settings.parent) { 10267 DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id); 10268 } 10269 co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')}); 10270 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container}); 10271 10272 if (s.menu_line) 10273 DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'}); 10274 10275 // n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'}); 10276 n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0}); 10277 tb = DOM.add(n, 'tbody'); 10278 10279 each(t.items, function(o) { 10280 t._add(tb, o); 10281 }); 10282 10283 t.rendered = true; 10284 10285 return w; 10286 }, 10287 10288 // Internal functions 10289 _setupKeyboardNav : function(){ 10290 var contextMenu, menuItems, t=this; 10291 contextMenu = DOM.get('menu_' + t.id); 10292 menuItems = DOM.select('a[role=option]', 'menu_' + t.id); 10293 menuItems.splice(0,0,contextMenu); 10294 t.keyboardNav = new tinymce.ui.KeyboardNavigation({ 10295 root: 'menu_' + t.id, 10296 items: menuItems, 10297 onCancel: function() { 10298 t.hideMenu(); 10299 }, 10300 enableUpDown: true 10301 }); 10302 contextMenu.focus(); 10303 }, 10304 10305 _keyHandler : function(evt) { 10306 var t = this, e; 10307 switch (evt.keyCode) { 10308 case 37: // Left 10309 if (t.settings.parent) { 10310 t.hideMenu(); 10311 t.settings.parent.focus(); 10312 Event.cancel(evt); 10313 } 10314 break; 10315 case 39: // Right 10316 if (t.mouseOverFunc) 10317 t.mouseOverFunc(evt); 10318 break; 10319 } 10320 }, 10321 10322 _add : function(tb, o) { 10323 var n, s = o.settings, a, ro, it, cp = this.classPrefix, ic; 10324 10325 if (s.separator) { 10326 ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'ItemSeparator'}); 10327 DOM.add(ro, 'td', {'class' : cp + 'ItemSeparator'}); 10328 10329 if (n = ro.previousSibling) 10330 DOM.addClass(n, 'mceLast'); 10331 10332 return; 10333 } 10334 10335 n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'}); 10336 n = it = DOM.add(n, s.titleItem ? 'th' : 'td'); 10337 n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'}); 10338 10339 if (s.parent) { 10340 DOM.setAttrib(a, 'aria-haspopup', 'true'); 10341 DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id); 10342 } 10343 10344 DOM.addClass(it, s['class']); 10345 // n = DOM.add(n, 'span', {'class' : 'item'}); 10346 10347 ic = DOM.add(n, 'span', {'class' : 'mceIcon' + (s.icon ? ' mce_' + s.icon : '')}); 10348 10349 if (s.icon_src) 10350 DOM.add(ic, 'img', {src : s.icon_src}); 10351 10352 n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title); 10353 10354 if (o.settings.style) { 10355 if (typeof o.settings.style == "function") 10356 o.settings.style = o.settings.style(); 10357 10358 DOM.setAttrib(n, 'style', o.settings.style); 10359 } 10360 10361 if (tb.childNodes.length == 1) 10362 DOM.addClass(ro, 'mceFirst'); 10363 10364 if ((n = ro.previousSibling) && DOM.hasClass(n, cp + 'ItemSeparator')) 10365 DOM.addClass(ro, 'mceFirst'); 10366 10367 if (o.collapse) 10368 DOM.addClass(ro, cp + 'ItemSub'); 10369 10370 if (n = ro.previousSibling) 10371 DOM.removeClass(n, 'mceLast'); 10372 10373 DOM.addClass(ro, 'mceLast'); 10374 } 10375 }); 10376 })(tinymce); 10377 (function(tinymce) { 10378 var DOM = tinymce.DOM; 10379 10380 tinymce.create('tinymce.ui.Button:tinymce.ui.Control', { 10381 Button : function(id, s, ed) { 10382 this.parent(id, s, ed); 10383 this.classPrefix = 'mceButton'; 10384 }, 10385 10386 renderHTML : function() { 10387 var cp = this.classPrefix, s = this.settings, h, l; 10388 10389 l = DOM.encode(s.label || ''); 10390 h = '<a role="button" id="' + this.id + '" href="javascript:;" class="' + cp + ' ' + cp + 'Enabled ' + s['class'] + (l ? ' ' + cp + 'Labeled' : '') +'" onmousedown="return false;" onclick="return false;" aria-labelledby="' + this.id + '_voice" title="' + DOM.encode(s.title) + '">'; 10391 if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) ) 10392 h += '<span class="mceIcon ' + s['class'] + '"><img class="mceIcon" src="' + s.image + '" alt="' + DOM.encode(s.title) + '" /></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : ''); 10393 else 10394 h += '<span class="mceIcon ' + s['class'] + '"></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : ''); 10395 10396 h += '<span class="mceVoiceLabel mceIconOnly" style="display: none;" id="' + this.id + '_voice">' + s.title + '</span>'; 10397 h += '</a>'; 10398 return h; 10399 }, 10400 10401 postRender : function() { 10402 var t = this, s = t.settings, imgBookmark; 10403 10404 // In IE a large image that occupies the entire editor area will be deselected when a button is clicked, so 10405 // need to keep the selection in case the selection is lost 10406 if (tinymce.isIE && t.editor) { 10407 tinymce.dom.Event.add(t.id, 'mousedown', function(e) { 10408 var nodeName = t.editor.selection.getNode().nodeName; 10409 imgBookmark = nodeName === 'IMG' ? t.editor.selection.getBookmark() : null; 10410 }); 10411 } 10412 tinymce.dom.Event.add(t.id, 'click', function(e) { 10413 if (!t.isDisabled()) { 10414 // restore the selection in case the selection is lost in IE 10415 if (tinymce.isIE && t.editor && imgBookmark !== null) { 10416 t.editor.selection.moveToBookmark(imgBookmark); 10417 } 10418 return s.onclick.call(s.scope, e); 10419 } 10420 }); 10421 tinymce.dom.Event.add(t.id, 'keyup', function(e) { 10422 if (!t.isDisabled() && e.keyCode==tinymce.VK.SPACEBAR) 10423 return s.onclick.call(s.scope, e); 10424 }); 10425 } 10426 }); 10427 })(tinymce); 10428 10429 (function(tinymce) { 10430 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, undef; 10431 10432 tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', { 10433 ListBox : function(id, s, ed) { 10434 var t = this; 10435 10436 t.parent(id, s, ed); 10437 10438 t.items = []; 10439 10440 t.onChange = new Dispatcher(t); 10441 10442 t.onPostRender = new Dispatcher(t); 10443 10444 t.onAdd = new Dispatcher(t); 10445 10446 t.onRenderMenu = new tinymce.util.Dispatcher(this); 10447 10448 t.classPrefix = 'mceListBox'; 10449 t.marked = {}; 10450 }, 10451 10452 select : function(va) { 10453 var t = this, fv, f; 10454 10455 t.marked = {}; 10456 10457 if (va == undef) 10458 return t.selectByIndex(-1); 10459 10460 // Is string or number make function selector 10461 if (va && typeof(va)=="function") 10462 f = va; 10463 else { 10464 f = function(v) { 10465 return v == va; 10466 }; 10467 } 10468 10469 // Do we need to do something? 10470 if (va != t.selectedValue) { 10471 // Find item 10472 each(t.items, function(o, i) { 10473 if (f(o.value)) { 10474 fv = 1; 10475 t.selectByIndex(i); 10476 return false; 10477 } 10478 }); 10479 10480 if (!fv) 10481 t.selectByIndex(-1); 10482 } 10483 }, 10484 10485 selectByIndex : function(idx) { 10486 var t = this, e, o, label; 10487 10488 t.marked = {}; 10489 10490 if (idx != t.selectedIndex) { 10491 e = DOM.get(t.id + '_text'); 10492 label = DOM.get(t.id + '_voiceDesc'); 10493 o = t.items[idx]; 10494 10495 if (o) { 10496 t.selectedValue = o.value; 10497 t.selectedIndex = idx; 10498 DOM.setHTML(e, DOM.encode(o.title)); 10499 DOM.setHTML(label, t.settings.title + " - " + o.title); 10500 DOM.removeClass(e, 'mceTitle'); 10501 DOM.setAttrib(t.id, 'aria-valuenow', o.title); 10502 } else { 10503 DOM.setHTML(e, DOM.encode(t.settings.title)); 10504 DOM.setHTML(label, DOM.encode(t.settings.title)); 10505 DOM.addClass(e, 'mceTitle'); 10506 t.selectedValue = t.selectedIndex = null; 10507 DOM.setAttrib(t.id, 'aria-valuenow', t.settings.title); 10508 } 10509 e = 0; 10510 } 10511 }, 10512 10513 mark : function(value) { 10514 this.marked[value] = true; 10515 }, 10516 10517 add : function(n, v, o) { 10518 var t = this; 10519 10520 o = o || {}; 10521 o = tinymce.extend(o, { 10522 title : n, 10523 value : v 10524 }); 10525 10526 t.items.push(o); 10527 t.onAdd.dispatch(t, o); 10528 }, 10529 10530 getLength : function() { 10531 return this.items.length; 10532 }, 10533 10534 renderHTML : function() { 10535 var h = '', t = this, s = t.settings, cp = t.classPrefix; 10536 10537 h = '<span role="listbox" aria-haspopup="true" aria-labelledby="' + t.id +'_voiceDesc" aria-describedby="' + t.id + '_voiceDesc"><table role="presentation" tabindex="0" id="' + t.id + '" cellpadding="0" cellspacing="0" class="' + cp + ' ' + cp + 'Enabled' + (s['class'] ? (' ' + s['class']) : '') + '"><tbody><tr>'; 10538 h += '<td>' + DOM.createHTML('span', {id: t.id + '_voiceDesc', 'class': 'voiceLabel', style:'display:none;'}, t.settings.title); 10539 h += DOM.createHTML('a', {id : t.id + '_text', tabindex : -1, href : 'javascript:;', 'class' : 'mceText', onclick : "return false;", onmousedown : 'return false;'}, DOM.encode(t.settings.title)) + '</td>'; 10540 h += '<td>' + DOM.createHTML('a', {id : t.id + '_open', tabindex : -1, href : 'javascript:;', 'class' : 'mceOpen', onclick : "return false;", onmousedown : 'return false;'}, '<span><span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span></span>') + '</td>'; 10541 h += '</tr></tbody></table></span>'; 10542 10543 return h; 10544 }, 10545 10546 showMenu : function() { 10547 var t = this, p2, e = DOM.get(this.id), m; 10548 10549 if (t.isDisabled() || t.items.length === 0) 10550 return; 10551 10552 if (t.menu && t.menu.isMenuVisible) 10553 return t.hideMenu(); 10554 10555 if (!t.isMenuRendered) { 10556 t.renderMenu(); 10557 t.isMenuRendered = true; 10558 } 10559 10560 p2 = DOM.getPos(e); 10561 10562 m = t.menu; 10563 m.settings.offset_x = p2.x; 10564 m.settings.offset_y = p2.y; 10565 m.settings.keyboard_focus = !tinymce.isOpera; // Opera is buggy when it comes to auto focus 10566 10567 // Select in menu 10568 each(t.items, function(o) { 10569 if (m.items[o.id]) { 10570 m.items[o.id].setSelected(0); 10571 } 10572 }); 10573 10574 each(t.items, function(o) { 10575 if (m.items[o.id] && t.marked[o.value]) { 10576 m.items[o.id].setSelected(1); 10577 } 10578 10579 if (o.value === t.selectedValue) { 10580 m.items[o.id].setSelected(1); 10581 } 10582 }); 10583 10584 m.showMenu(0, e.clientHeight); 10585 10586 Event.add(DOM.doc, 'mousedown', t.hideMenu, t); 10587 DOM.addClass(t.id, t.classPrefix + 'Selected'); 10588 10589 //DOM.get(t.id + '_text').focus(); 10590 }, 10591 10592 hideMenu : function(e) { 10593 var t = this; 10594 10595 if (t.menu && t.menu.isMenuVisible) { 10596 DOM.removeClass(t.id, t.classPrefix + 'Selected'); 10597 10598 // Prevent double toogles by canceling the mouse click event to the button 10599 if (e && e.type == "mousedown" && (e.target.id == t.id + '_text' || e.target.id == t.id + '_open')) 10600 return; 10601 10602 if (!e || !DOM.getParent(e.target, '.mceMenu')) { 10603 DOM.removeClass(t.id, t.classPrefix + 'Selected'); 10604 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); 10605 t.menu.hideMenu(); 10606 } 10607 } 10608 }, 10609 10610 renderMenu : function() { 10611 var t = this, m; 10612 10613 m = t.settings.control_manager.createDropMenu(t.id + '_menu', { 10614 menu_line : 1, 10615 'class' : t.classPrefix + 'Menu mceNoIcons', 10616 max_width : 250, 10617 max_height : 150 10618 }); 10619 10620 m.onHideMenu.add(function() { 10621 t.hideMenu(); 10622 t.focus(); 10623 }); 10624 10625 m.add({ 10626 title : t.settings.title, 10627 'class' : 'mceMenuItemTitle', 10628 onclick : function() { 10629 if (t.settings.onselect('') !== false) 10630 t.select(''); // Must be runned after 10631 } 10632 }); 10633 10634 each(t.items, function(o) { 10635 // No value then treat it as a title 10636 if (o.value === undef) { 10637 m.add({ 10638 title : o.title, 10639 role : "option", 10640 'class' : 'mceMenuItemTitle', 10641 onclick : function() { 10642 if (t.settings.onselect('') !== false) 10643 t.select(''); // Must be runned after 10644 } 10645 }); 10646 } else { 10647 o.id = DOM.uniqueId(); 10648 o.role= "option"; 10649 o.onclick = function() { 10650 if (t.settings.onselect(o.value) !== false) 10651 t.select(o.value); // Must be runned after 10652 }; 10653 10654 m.add(o); 10655 } 10656 }); 10657 10658 t.onRenderMenu.dispatch(t, m); 10659 t.menu = m; 10660 }, 10661 10662 postRender : function() { 10663 var t = this, cp = t.classPrefix; 10664 10665 Event.add(t.id, 'click', t.showMenu, t); 10666 Event.add(t.id, 'keydown', function(evt) { 10667 if (evt.keyCode == 32) { // Space 10668 t.showMenu(evt); 10669 Event.cancel(evt); 10670 } 10671 }); 10672 Event.add(t.id, 'focus', function() { 10673 if (!t._focused) { 10674 t.keyDownHandler = Event.add(t.id, 'keydown', function(e) { 10675 if (e.keyCode == 40) { 10676 t.showMenu(); 10677 Event.cancel(e); 10678 } 10679 }); 10680 t.keyPressHandler = Event.add(t.id, 'keypress', function(e) { 10681 var v; 10682 if (e.keyCode == 13) { 10683 // Fake select on enter 10684 v = t.selectedValue; 10685 t.selectedValue = null; // Needs to be null to fake change 10686 Event.cancel(e); 10687 t.settings.onselect(v); 10688 } 10689 }); 10690 } 10691 10692 t._focused = 1; 10693 }); 10694 Event.add(t.id, 'blur', function() { 10695 Event.remove(t.id, 'keydown', t.keyDownHandler); 10696 Event.remove(t.id, 'keypress', t.keyPressHandler); 10697 t._focused = 0; 10698 }); 10699 10700 // Old IE doesn't have hover on all elements 10701 if (tinymce.isIE6 || !DOM.boxModel) { 10702 Event.add(t.id, 'mouseover', function() { 10703 if (!DOM.hasClass(t.id, cp + 'Disabled')) 10704 DOM.addClass(t.id, cp + 'Hover'); 10705 }); 10706 10707 Event.add(t.id, 'mouseout', function() { 10708 if (!DOM.hasClass(t.id, cp + 'Disabled')) 10709 DOM.removeClass(t.id, cp + 'Hover'); 10710 }); 10711 } 10712 10713 t.onPostRender.dispatch(t, DOM.get(t.id)); 10714 }, 10715 10716 destroy : function() { 10717 this.parent(); 10718 10719 Event.clear(this.id + '_text'); 10720 Event.clear(this.id + '_open'); 10721 } 10722 }); 10723 })(tinymce); 10724 10725 (function(tinymce) { 10726 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, undef; 10727 10728 tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', { 10729 NativeListBox : function(id, s) { 10730 this.parent(id, s); 10731 this.classPrefix = 'mceNativeListBox'; 10732 }, 10733 10734 setDisabled : function(s) { 10735 DOM.get(this.id).disabled = s; 10736 this.setAriaProperty('disabled', s); 10737 }, 10738 10739 isDisabled : function() { 10740 return DOM.get(this.id).disabled; 10741 }, 10742 10743 select : function(va) { 10744 var t = this, fv, f; 10745 10746 if (va == undef) 10747 return t.selectByIndex(-1); 10748 10749 // Is string or number make function selector 10750 if (va && typeof(va)=="function") 10751 f = va; 10752 else { 10753 f = function(v) { 10754 return v == va; 10755 }; 10756 } 10757 10758 // Do we need to do something? 10759 if (va != t.selectedValue) { 10760 // Find item 10761 each(t.items, function(o, i) { 10762 if (f(o.value)) { 10763 fv = 1; 10764 t.selectByIndex(i); 10765 return false; 10766 } 10767 }); 10768 10769 if (!fv) 10770 t.selectByIndex(-1); 10771 } 10772 }, 10773 10774 selectByIndex : function(idx) { 10775 DOM.get(this.id).selectedIndex = idx + 1; 10776 this.selectedValue = this.items[idx] ? this.items[idx].value : null; 10777 }, 10778 10779 add : function(n, v, a) { 10780 var o, t = this; 10781 10782 a = a || {}; 10783 a.value = v; 10784 10785 if (t.isRendered()) 10786 DOM.add(DOM.get(this.id), 'option', a, n); 10787 10788 o = { 10789 title : n, 10790 value : v, 10791 attribs : a 10792 }; 10793 10794 t.items.push(o); 10795 t.onAdd.dispatch(t, o); 10796 }, 10797 10798 getLength : function() { 10799 return this.items.length; 10800 }, 10801 10802 renderHTML : function() { 10803 var h, t = this; 10804 10805 h = DOM.createHTML('option', {value : ''}, '-- ' + t.settings.title + ' --'); 10806 10807 each(t.items, function(it) { 10808 h += DOM.createHTML('option', {value : it.value}, it.title); 10809 }); 10810 10811 h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox', 'aria-labelledby': t.id + '_aria'}, h); 10812 h += DOM.createHTML('span', {id : t.id + '_aria', 'style': 'display: none'}, t.settings.title); 10813 return h; 10814 }, 10815 10816 postRender : function() { 10817 var t = this, ch, changeListenerAdded = true; 10818 10819 t.rendered = true; 10820 10821 function onChange(e) { 10822 var v = t.items[e.target.selectedIndex - 1]; 10823 10824 if (v && (v = v.value)) { 10825 t.onChange.dispatch(t, v); 10826 10827 if (t.settings.onselect) 10828 t.settings.onselect(v); 10829 } 10830 }; 10831 10832 Event.add(t.id, 'change', onChange); 10833 10834 // Accessibility keyhandler 10835 Event.add(t.id, 'keydown', function(e) { 10836 var bf; 10837 10838 Event.remove(t.id, 'change', ch); 10839 changeListenerAdded = false; 10840 10841 bf = Event.add(t.id, 'blur', function() { 10842 if (changeListenerAdded) return; 10843 changeListenerAdded = true; 10844 Event.add(t.id, 'change', onChange); 10845 Event.remove(t.id, 'blur', bf); 10846 }); 10847 10848 //prevent default left and right keys on chrome - so that the keyboard navigation is used. 10849 if (tinymce.isWebKit && (e.keyCode==37 ||e.keyCode==39)) { 10850 return Event.prevent(e); 10851 } 10852 10853 if (e.keyCode == 13 || e.keyCode == 32) { 10854 onChange(e); 10855 return Event.cancel(e); 10856 } 10857 }); 10858 10859 t.onPostRender.dispatch(t, DOM.get(t.id)); 10860 } 10861 }); 10862 })(tinymce); 10863 10864 (function(tinymce) { 10865 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; 10866 10867 tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', { 10868 MenuButton : function(id, s, ed) { 10869 this.parent(id, s, ed); 10870 10871 this.onRenderMenu = new tinymce.util.Dispatcher(this); 10872 10873 s.menu_container = s.menu_container || DOM.doc.body; 10874 }, 10875 10876 showMenu : function() { 10877 var t = this, p1, p2, e = DOM.get(t.id), m; 10878 10879 if (t.isDisabled()) 10880 return; 10881 10882 if (!t.isMenuRendered) { 10883 t.renderMenu(); 10884 t.isMenuRendered = true; 10885 } 10886 10887 if (t.isMenuVisible) 10888 return t.hideMenu(); 10889 10890 p1 = DOM.getPos(t.settings.menu_container); 10891 p2 = DOM.getPos(e); 10892 10893 m = t.menu; 10894 m.settings.offset_x = p2.x; 10895 m.settings.offset_y = p2.y; 10896 m.settings.vp_offset_x = p2.x; 10897 m.settings.vp_offset_y = p2.y; 10898 m.settings.keyboard_focus = t._focused; 10899 m.showMenu(0, e.firstChild.clientHeight); 10900 10901 Event.add(DOM.doc, 'mousedown', t.hideMenu, t); 10902 t.setState('Selected', 1); 10903 10904 t.isMenuVisible = 1; 10905 }, 10906 10907 renderMenu : function() { 10908 var t = this, m; 10909 10910 m = t.settings.control_manager.createDropMenu(t.id + '_menu', { 10911 menu_line : 1, 10912 'class' : this.classPrefix + 'Menu', 10913 icons : t.settings.icons 10914 }); 10915 10916 m.onHideMenu.add(function() { 10917 t.hideMenu(); 10918 t.focus(); 10919 }); 10920 10921 t.onRenderMenu.dispatch(t, m); 10922 t.menu = m; 10923 }, 10924 10925 hideMenu : function(e) { 10926 var t = this; 10927 10928 // Prevent double toogles by canceling the mouse click event to the button 10929 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id || e.id === t.id + '_open';})) 10930 return; 10931 10932 if (!e || !DOM.getParent(e.target, '.mceMenu')) { 10933 t.setState('Selected', 0); 10934 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); 10935 if (t.menu) 10936 t.menu.hideMenu(); 10937 } 10938 10939 t.isMenuVisible = 0; 10940 }, 10941 10942 postRender : function() { 10943 var t = this, s = t.settings; 10944 10945 Event.add(t.id, 'click', function() { 10946 if (!t.isDisabled()) { 10947 if (s.onclick) 10948 s.onclick(t.value); 10949 10950 t.showMenu(); 10951 } 10952 }); 10953 } 10954 }); 10955 })(tinymce); 10956 10957 (function(tinymce) { 10958 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; 10959 10960 tinymce.create('tinymce.ui.SplitButton:tinymce.ui.MenuButton', { 10961 SplitButton : function(id, s, ed) { 10962 this.parent(id, s, ed); 10963 this.classPrefix = 'mceSplitButton'; 10964 }, 10965 10966 renderHTML : function() { 10967 var h, t = this, s = t.settings, h1; 10968 10969 h = '<tbody><tr>'; 10970 10971 if (s.image) 10972 h1 = DOM.createHTML('img ', {src : s.image, role: 'presentation', 'class' : 'mceAction ' + s['class']}); 10973 else 10974 h1 = DOM.createHTML('span', {'class' : 'mceAction ' + s['class']}, ''); 10975 10976 h1 += DOM.createHTML('span', {'class': 'mceVoiceLabel mceIconOnly', id: t.id + '_voice', style: 'display:none;'}, s.title); 10977 h += '<td >' + DOM.createHTML('a', {role: 'button', id : t.id + '_action', tabindex: '-1', href : 'javascript:;', 'class' : 'mceAction ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '</td>'; 10978 10979 h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}, '<span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span>'); 10980 h += '<td >' + DOM.createHTML('a', {role: 'button', id : t.id + '_open', tabindex: '-1', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '</td>'; 10981 10982 h += '</tr></tbody>'; 10983 h = DOM.createHTML('table', { role: 'presentation', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h); 10984 return DOM.createHTML('div', {id : t.id, role: 'button', tabindex: '0', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h); 10985 }, 10986 10987 postRender : function() { 10988 var t = this, s = t.settings, activate; 10989 10990 if (s.onclick) { 10991 activate = function(evt) { 10992 if (!t.isDisabled()) { 10993 s.onclick(t.value); 10994 Event.cancel(evt); 10995 } 10996 }; 10997 Event.add(t.id + '_action', 'click', activate); 10998 Event.add(t.id, ['click', 'keydown'], function(evt) { 10999 var DOM_VK_SPACE = 32, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_UP = 38, DOM_VK_DOWN = 40; 11000 if ((evt.keyCode === 32 || evt.keyCode === 13 || evt.keyCode === 14) && !evt.altKey && !evt.ctrlKey && !evt.metaKey) { 11001 activate(); 11002 Event.cancel(evt); 11003 } else if (evt.type === 'click' || evt.keyCode === DOM_VK_DOWN) { 11004 t.showMenu(); 11005 Event.cancel(evt); 11006 } 11007 }); 11008 } 11009 11010 Event.add(t.id + '_open', 'click', function (evt) { 11011 t.showMenu(); 11012 Event.cancel(evt); 11013 }); 11014 Event.add([t.id, t.id + '_open'], 'focus', function() {t._focused = 1;}); 11015 Event.add([t.id, t.id + '_open'], 'blur', function() {t._focused = 0;}); 11016 11017 // Old IE doesn't have hover on all elements 11018 if (tinymce.isIE6 || !DOM.boxModel) { 11019 Event.add(t.id, 'mouseover', function() { 11020 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled')) 11021 DOM.addClass(t.id, 'mceSplitButtonHover'); 11022 }); 11023 11024 Event.add(t.id, 'mouseout', function() { 11025 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled')) 11026 DOM.removeClass(t.id, 'mceSplitButtonHover'); 11027 }); 11028 } 11029 }, 11030 11031 destroy : function() { 11032 this.parent(); 11033 11034 Event.clear(this.id + '_action'); 11035 Event.clear(this.id + '_open'); 11036 Event.clear(this.id); 11037 } 11038 }); 11039 })(tinymce); 11040 11041 (function(tinymce) { 11042 var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each; 11043 11044 tinymce.create('tinymce.ui.ColorSplitButton:tinymce.ui.SplitButton', { 11045 ColorSplitButton : function(id, s, ed) { 11046 var t = this; 11047 11048 t.parent(id, s, ed); 11049 11050 t.settings = s = tinymce.extend({ 11051 colors : '000000,993300,333300,003300,003366,000080,333399,333333,800000,FF6600,808000,008000,008080,0000FF,666699,808080,FF0000,FF9900,99CC00,339966,33CCCC,3366FF,800080,999999,FF00FF,FFCC00,FFFF00,00FF00,00FFFF,00CCFF,993366,C0C0C0,FF99CC,FFCC99,FFFF99,CCFFCC,CCFFFF,99CCFF,CC99FF,FFFFFF', 11052 grid_width : 8, 11053 default_color : '#888888' 11054 }, t.settings); 11055 11056 t.onShowMenu = new tinymce.util.Dispatcher(t); 11057 11058 t.onHideMenu = new tinymce.util.Dispatcher(t); 11059 11060 t.value = s.default_color; 11061 }, 11062 11063 showMenu : function() { 11064 var t = this, r, p, e, p2; 11065 11066 if (t.isDisabled()) 11067 return; 11068 11069 if (!t.isMenuRendered) { 11070 t.renderMenu(); 11071 t.isMenuRendered = true; 11072 } 11073 11074 if (t.isMenuVisible) 11075 return t.hideMenu(); 11076 11077 e = DOM.get(t.id); 11078 DOM.show(t.id + '_menu'); 11079 DOM.addClass(e, 'mceSplitButtonSelected'); 11080 p2 = DOM.getPos(e); 11081 DOM.setStyles(t.id + '_menu', { 11082 left : p2.x, 11083 top : p2.y + e.firstChild.clientHeight, 11084 zIndex : 200000 11085 }); 11086 e = 0; 11087 11088 Event.add(DOM.doc, 'mousedown', t.hideMenu, t); 11089 t.onShowMenu.dispatch(t); 11090 11091 if (t._focused) { 11092 t._keyHandler = Event.add(t.id + '_menu', 'keydown', function(e) { 11093 if (e.keyCode == 27) 11094 t.hideMenu(); 11095 }); 11096 11097 DOM.select('a', t.id + '_menu')[0].focus(); // Select first link 11098 } 11099 11100 t.keyboardNav = new tinymce.ui.KeyboardNavigation({ 11101 root: t.id + '_menu', 11102 items: DOM.select('a', t.id + '_menu'), 11103 onCancel: function() { 11104 t.hideMenu(); 11105 t.focus(); 11106 } 11107 }); 11108 11109 t.keyboardNav.focus(); 11110 t.isMenuVisible = 1; 11111 }, 11112 11113 hideMenu : function(e) { 11114 var t = this; 11115 11116 if (t.isMenuVisible) { 11117 // Prevent double toogles by canceling the mouse click event to the button 11118 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';})) 11119 return; 11120 11121 if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) { 11122 DOM.removeClass(t.id, 'mceSplitButtonSelected'); 11123 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); 11124 Event.remove(t.id + '_menu', 'keydown', t._keyHandler); 11125 DOM.hide(t.id + '_menu'); 11126 } 11127 11128 t.isMenuVisible = 0; 11129 t.onHideMenu.dispatch(); 11130 t.keyboardNav.destroy(); 11131 } 11132 }, 11133 11134 renderMenu : function() { 11135 var t = this, m, i = 0, s = t.settings, n, tb, tr, w, context; 11136 11137 w = DOM.add(s.menu_container, 'div', {role: 'listbox', id : t.id + '_menu', 'class' : s.menu_class + ' ' + s['class'], style : 'position:absolute;left:0;top:-1000px;'}); 11138 m = DOM.add(w, 'div', {'class' : s['class'] + ' mceSplitButtonMenu'}); 11139 DOM.add(m, 'span', {'class' : 'mceMenuLine'}); 11140 11141 n = DOM.add(m, 'table', {role: 'presentation', 'class' : 'mceColorSplitMenu'}); 11142 tb = DOM.add(n, 'tbody'); 11143 11144 // Generate color grid 11145 i = 0; 11146 each(is(s.colors, 'array') ? s.colors : s.colors.split(','), function(c) { 11147 c = c.replace(/^#/, ''); 11148 11149 if (!i--) { 11150 tr = DOM.add(tb, 'tr'); 11151 i = s.grid_width - 1; 11152 } 11153 11154 n = DOM.add(tr, 'td'); 11155 var settings = { 11156 href : 'javascript:;', 11157 style : { 11158 backgroundColor : '#' + c 11159 }, 11160 'title': t.editor.getLang('colors.' + c, c), 11161 'data-mce-color' : '#' + c 11162 }; 11163 11164 // adding a proper ARIA role = button causes JAWS to read things incorrectly on IE. 11165 if (!tinymce.isIE ) { 11166 settings.role = 'option'; 11167 } 11168 11169 n = DOM.add(n, 'a', settings); 11170 11171 if (t.editor.forcedHighContrastMode) { 11172 n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' }); 11173 if (n.getContext && (context = n.getContext("2d"))) { 11174 context.fillStyle = '#' + c; 11175 context.fillRect(0, 0, 16, 16); 11176 } else { 11177 // No point leaving a canvas element around if it's not supported for drawing on anyway. 11178 DOM.remove(n); 11179 } 11180 } 11181 }); 11182 11183 if (s.more_colors_func) { 11184 n = DOM.add(tb, 'tr'); 11185 n = DOM.add(n, 'td', {colspan : s.grid_width, 'class' : 'mceMoreColors'}); 11186 n = DOM.add(n, 'a', {role: 'option', id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title); 11187 11188 Event.add(n, 'click', function(e) { 11189 s.more_colors_func.call(s.more_colors_scope || this); 11190 return Event.cancel(e); // Cancel to fix onbeforeunload problem 11191 }); 11192 } 11193 11194 DOM.addClass(m, 'mceColorSplitMenu'); 11195 11196 // Prevent IE from scrolling and hindering click to occur #4019 11197 Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);}); 11198 11199 Event.add(t.id + '_menu', 'click', function(e) { 11200 var c; 11201 11202 e = DOM.getParent(e.target, 'a', tb); 11203 11204 if (e && e.nodeName.toLowerCase() == 'a' && (c = e.getAttribute('data-mce-color'))) 11205 t.setColor(c); 11206 11207 return false; // Prevent IE auto save warning 11208 }); 11209 11210 return w; 11211 }, 11212 11213 setColor : function(c) { 11214 this.displayColor(c); 11215 this.hideMenu(); 11216 this.settings.onselect(c); 11217 }, 11218 11219 displayColor : function(c) { 11220 var t = this; 11221 11222 DOM.setStyle(t.id + '_preview', 'backgroundColor', c); 11223 11224 t.value = c; 11225 }, 11226 11227 postRender : function() { 11228 var t = this, id = t.id; 11229 11230 t.parent(); 11231 DOM.add(id + '_action', 'div', {id : id + '_preview', 'class' : 'mceColorPreview'}); 11232 DOM.setStyle(t.id + '_preview', 'backgroundColor', t.value); 11233 }, 11234 11235 destroy : function() { 11236 var self = this; 11237 11238 self.parent(); 11239 11240 Event.clear(self.id + '_menu'); 11241 Event.clear(self.id + '_more'); 11242 DOM.remove(self.id + '_menu'); 11243 11244 if (self.keyboardNav) { 11245 self.keyboardNav.destroy(); 11246 } 11247 } 11248 }); 11249 })(tinymce); 11250 11251 (function(tinymce) { 11252 // Shorten class names 11253 var dom = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event; 11254 tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', { 11255 renderHTML : function() { 11256 var t = this, h = [], controls = t.controls, each = tinymce.each, settings = t.settings; 11257 11258 h.push('<div id="' + t.id + '" role="group" aria-labelledby="' + t.id + '_voice">'); 11259 //TODO: ACC test this out - adding a role = application for getting the landmarks working well. 11260 h.push("<span role='application'>"); 11261 h.push('<span id="' + t.id + '_voice" class="mceVoiceLabel" style="display:none;">' + dom.encode(settings.name) + '</span>'); 11262 each(controls, function(toolbar) { 11263 h.push(toolbar.renderHTML()); 11264 }); 11265 h.push("</span>"); 11266 h.push('</div>'); 11267 11268 return h.join(''); 11269 }, 11270 11271 focus : function() { 11272 var t = this; 11273 dom.get(t.id).focus(); 11274 }, 11275 11276 postRender : function() { 11277 var t = this, items = []; 11278 11279 each(t.controls, function(toolbar) { 11280 each (toolbar.controls, function(control) { 11281 if (control.id) { 11282 items.push(control); 11283 } 11284 }); 11285 }); 11286 11287 t.keyNav = new tinymce.ui.KeyboardNavigation({ 11288 root: t.id, 11289 items: items, 11290 onCancel: function() { 11291 //Move focus if webkit so that navigation back will read the item. 11292 if (tinymce.isWebKit) { 11293 dom.get(t.editor.id+"_ifr").focus(); 11294 } 11295 t.editor.focus(); 11296 }, 11297 excludeFromTabOrder: !t.settings.tab_focus_toolbar 11298 }); 11299 }, 11300 11301 destroy : function() { 11302 var self = this; 11303 11304 self.parent(); 11305 self.keyNav.destroy(); 11306 Event.clear(self.id); 11307 } 11308 }); 11309 })(tinymce); 11310 11311 (function(tinymce) { 11312 // Shorten class names 11313 var dom = tinymce.DOM, each = tinymce.each; 11314 tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { 11315 renderHTML : function() { 11316 var t = this, h = '', c, co, s = t.settings, i, pr, nx, cl; 11317 11318 cl = t.controls; 11319 for (i=0; i<cl.length; i++) { 11320 // Get current control, prev control, next control and if the control is a list box or not 11321 co = cl[i]; 11322 pr = cl[i - 1]; 11323 nx = cl[i + 1]; 11324 11325 // Add toolbar start 11326 if (i === 0) { 11327 c = 'mceToolbarStart'; 11328 11329 if (co.Button) 11330 c += ' mceToolbarStartButton'; 11331 else if (co.SplitButton) 11332 c += ' mceToolbarStartSplitButton'; 11333 else if (co.ListBox) 11334 c += ' mceToolbarStartListBox'; 11335 11336 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->')); 11337 } 11338 11339 // Add toolbar end before list box and after the previous button 11340 // This is to fix the o2k7 editor skins 11341 if (pr && co.ListBox) { 11342 if (pr.Button || pr.SplitButton) 11343 h += dom.createHTML('td', {'class' : 'mceToolbarEnd'}, dom.createHTML('span', null, '<!-- IE -->')); 11344 } 11345 11346 // Render control HTML 11347 11348 // IE 8 quick fix, needed to propertly generate a hit area for anchors 11349 if (dom.stdMode) 11350 h += '<td style="position: relative">' + co.renderHTML() + '</td>'; 11351 else 11352 h += '<td>' + co.renderHTML() + '</td>'; 11353 11354 // Add toolbar start after list box and before the next button 11355 // This is to fix the o2k7 editor skins 11356 if (nx && co.ListBox) { 11357 if (nx.Button || nx.SplitButton) 11358 h += dom.createHTML('td', {'class' : 'mceToolbarStart'}, dom.createHTML('span', null, '<!-- IE -->')); 11359 } 11360 } 11361 11362 c = 'mceToolbarEnd'; 11363 11364 if (co.Button) 11365 c += ' mceToolbarEndButton'; 11366 else if (co.SplitButton) 11367 c += ' mceToolbarEndSplitButton'; 11368 else if (co.ListBox) 11369 c += ' mceToolbarEndListBox'; 11370 11371 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->')); 11372 11373 return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || '', role: 'presentation', tabindex: '-1'}, '<tbody><tr>' + h + '</tr></tbody>'); 11374 } 11375 }); 11376 })(tinymce); 11377 11378 (function(tinymce) { 11379 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each; 11380 11381 tinymce.create('tinymce.AddOnManager', { 11382 AddOnManager : function() { 11383 var self = this; 11384 11385 self.items = []; 11386 self.urls = {}; 11387 self.lookup = {}; 11388 self.onAdd = new Dispatcher(self); 11389 }, 11390 11391 get : function(n) { 11392 if (this.lookup[n]) { 11393 return this.lookup[n].instance; 11394 } else { 11395 return undefined; 11396 } 11397 }, 11398 11399 dependencies : function(n) { 11400 var result; 11401 if (this.lookup[n]) { 11402 result = this.lookup[n].dependencies; 11403 } 11404 return result || []; 11405 }, 11406 11407 requireLangPack : function(n) { 11408 var s = tinymce.settings; 11409 11410 if (s && s.language && s.language_load !== false) 11411 tinymce.ScriptLoader.add(this.urls[n] + '/langs/' + s.language + '.js'); 11412 }, 11413 11414 add : function(id, o, dependencies) { 11415 this.items.push(o); 11416 this.lookup[id] = {instance:o, dependencies:dependencies}; 11417 this.onAdd.dispatch(this, id, o); 11418 11419 return o; 11420 }, 11421 createUrl: function(baseUrl, dep) { 11422 if (typeof dep === "object") { 11423 return dep 11424 } else { 11425 return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix}; 11426 } 11427 }, 11428 11429 addComponents: function(pluginName, scripts) { 11430 var pluginUrl = this.urls[pluginName]; 11431 tinymce.each(scripts, function(script){ 11432 tinymce.ScriptLoader.add(pluginUrl+"/"+script); 11433 }); 11434 }, 11435 11436 load : function(n, u, cb, s) { 11437 var t = this, url = u; 11438 11439 function loadDependencies() { 11440 var dependencies = t.dependencies(n); 11441 tinymce.each(dependencies, function(dep) { 11442 var newUrl = t.createUrl(u, dep); 11443 t.load(newUrl.resource, newUrl, undefined, undefined); 11444 }); 11445 if (cb) { 11446 if (s) { 11447 cb.call(s); 11448 } else { 11449 cb.call(tinymce.ScriptLoader); 11450 } 11451 } 11452 } 11453 11454 if (t.urls[n]) 11455 return; 11456 if (typeof u === "object") 11457 url = u.prefix + u.resource + u.suffix; 11458 11459 if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) 11460 url = tinymce.baseURL + '/' + url; 11461 11462 t.urls[n] = url.substring(0, url.lastIndexOf('/')); 11463 11464 if (t.lookup[n]) { 11465 loadDependencies(); 11466 } else { 11467 tinymce.ScriptLoader.add(url, loadDependencies, s); 11468 } 11469 } 11470 }); 11471 11472 // Create plugin and theme managers 11473 tinymce.PluginManager = new tinymce.AddOnManager(); 11474 tinymce.ThemeManager = new tinymce.AddOnManager(); 11475 }(tinymce)); 11476 11477 (function(tinymce) { 11478 // Shorten names 11479 var each = tinymce.each, extend = tinymce.extend, 11480 DOM = tinymce.DOM, Event = tinymce.dom.Event, 11481 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, 11482 explode = tinymce.explode, 11483 Dispatcher = tinymce.util.Dispatcher, undef, instanceCounter = 0; 11484 11485 // Setup some URLs where the editor API is located and where the document is 11486 tinymce.documentBaseURL = window.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); 11487 if (!/[\/\\]$/.test(tinymce.documentBaseURL)) 11488 tinymce.documentBaseURL += '/'; 11489 11490 tinymce.baseURL = new tinymce.util.URI(tinymce.documentBaseURL).toAbsolute(tinymce.baseURL); 11491 11492 tinymce.baseURI = new tinymce.util.URI(tinymce.baseURL); 11493 11494 // Add before unload listener 11495 // This was required since IE was leaking memory if you added and removed beforeunload listeners 11496 // with attachEvent/detatchEvent so this only adds one listener and instances can the attach to the onBeforeUnload event 11497 tinymce.onBeforeUnload = new Dispatcher(tinymce); 11498 11499 // Must be on window or IE will leak if the editor is placed in frame or iframe 11500 Event.add(window, 'beforeunload', function(e) { 11501 tinymce.onBeforeUnload.dispatch(tinymce, e); 11502 }); 11503 11504 tinymce.onAddEditor = new Dispatcher(tinymce); 11505 11506 tinymce.onRemoveEditor = new Dispatcher(tinymce); 11507 11508 tinymce.EditorManager = extend(tinymce, { 11509 editors : [], 11510 11511 i18n : {}, 11512 11513 activeEditor : null, 11514 11515 init : function(s) { 11516 var t = this, pl, sl = tinymce.ScriptLoader, e, el = [], ed; 11517 11518 function createId(elm) { 11519 var id = elm.id; 11520 11521 // Use element id, or unique name or generate a unique id 11522 if (!id) { 11523 id = elm.name; 11524 11525 if (id && !DOM.get(id)) { 11526 id = elm.name; 11527 } else { 11528 // Generate unique name 11529 id = DOM.uniqueId(); 11530 } 11531 11532 elm.setAttribute('id', id); 11533 } 11534 11535 return id; 11536 }; 11537 11538 function execCallback(se, n, s) { 11539 var f = se[n]; 11540 11541 if (!f) 11542 return; 11543 11544 if (tinymce.is(f, 'string')) { 11545 s = f.replace(/\.\w+$/, ''); 11546 s = s ? tinymce.resolve(s) : 0; 11547 f = tinymce.resolve(f); 11548 } 11549 11550 return f.apply(s || this, Array.prototype.slice.call(arguments, 2)); 11551 }; 11552 11553 function hasClass(n, c) { 11554 return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c); 11555 }; 11556 11557 t.settings = s; 11558 11559 // Legacy call 11560 Event.bind(window, 'ready', function() { 11561 var l, co; 11562 11563 execCallback(s, 'onpageload'); 11564 11565 switch (s.mode) { 11566 case "exact": 11567 l = s.elements || ''; 11568 11569 if(l.length > 0) { 11570 each(explode(l), function(v) { 11571 if (DOM.get(v)) { 11572 ed = new tinymce.Editor(v, s); 11573 el.push(ed); 11574 ed.render(1); 11575 } else { 11576 each(document.forms, function(f) { 11577 each(f.elements, function(e) { 11578 if (e.name === v) { 11579 v = 'mce_editor_' + instanceCounter++; 11580 DOM.setAttrib(e, 'id', v); 11581 11582 ed = new tinymce.Editor(v, s); 11583 el.push(ed); 11584 ed.render(1); 11585 } 11586 }); 11587 }); 11588 } 11589 }); 11590 } 11591 break; 11592 11593 case "textareas": 11594 case "specific_textareas": 11595 each(DOM.select('textarea'), function(elm) { 11596 if (s.editor_deselector && hasClass(elm, s.editor_deselector)) 11597 return; 11598 11599 if (!s.editor_selector || hasClass(elm, s.editor_selector)) { 11600 ed = new tinymce.Editor(createId(elm), s); 11601 el.push(ed); 11602 ed.render(1); 11603 } 11604 }); 11605 break; 11606 11607 default: 11608 if (s.types) { 11609 // Process type specific selector 11610 each(s.types, function(type) { 11611 each(DOM.select(type.selector), function(elm) { 11612 var editor = new tinymce.Editor(createId(elm), tinymce.extend({}, s, type)); 11613 el.push(editor); 11614 editor.render(1); 11615 }); 11616 }); 11617 } else if (s.selector) { 11618 // Process global selector 11619 each(DOM.select(s.selector), function(elm) { 11620 var editor = new tinymce.Editor(createId(elm), s); 11621 el.push(editor); 11622 editor.render(1); 11623 }); 11624 } 11625 } 11626 11627 // Call onInit when all editors are initialized 11628 if (s.oninit) { 11629 l = co = 0; 11630 11631 each(el, function(ed) { 11632 co++; 11633 11634 if (!ed.initialized) { 11635 // Wait for it 11636 ed.onInit.add(function() { 11637 l++; 11638 11639 // All done 11640 if (l == co) 11641 execCallback(s, 'oninit'); 11642 }); 11643 } else 11644 l++; 11645 11646 // All done 11647 if (l == co) 11648 execCallback(s, 'oninit'); 11649 }); 11650 } 11651 }); 11652 }, 11653 11654 get : function(id) { 11655 if (id === undef) 11656 return this.editors; 11657 11658 return this.editors[id]; 11659 }, 11660 11661 getInstanceById : function(id) { 11662 return this.get(id); 11663 }, 11664 11665 add : function(editor) { 11666 var self = this, editors = self.editors; 11667 11668 // Add named and index editor instance 11669 editors[editor.id] = editor; 11670 editors.push(editor); 11671 11672 self._setActive(editor); 11673 self.onAddEditor.dispatch(self, editor); 11674 11675 11676 // Patch the tinymce.Editor instance with jQuery adapter logic 11677 if (tinymce.adapter) 11678 tinymce.adapter.patchEditor(editor); 11679 11680 11681 return editor; 11682 }, 11683 11684 remove : function(editor) { 11685 var t = this, i, editors = t.editors; 11686 11687 // Not in the collection 11688 if (!editors[editor.id]) 11689 return null; 11690 11691 delete editors[editor.id]; 11692 11693 for (i = 0; i < editors.length; i++) { 11694 if (editors[i] == editor) { 11695 editors.splice(i, 1); 11696 break; 11697 } 11698 } 11699 11700 // Select another editor since the active one was removed 11701 if (t.activeEditor == editor) 11702 t._setActive(editors[0]); 11703 11704 editor.destroy(); 11705 t.onRemoveEditor.dispatch(t, editor); 11706 11707 return editor; 11708 }, 11709 11710 execCommand : function(c, u, v) { 11711 var t = this, ed = t.get(v), w; 11712 11713 function clr() { 11714 ed.destroy(); 11715 w.detachEvent('onunload', clr); 11716 w = w.tinyMCE = w.tinymce = null; // IE leak 11717 }; 11718 11719 // Manager commands 11720 switch (c) { 11721 case "mceFocus": 11722 ed.focus(); 11723 return true; 11724 11725 case "mceAddEditor": 11726 case "mceAddControl": 11727 if (!t.get(v)) 11728 new tinymce.Editor(v, t.settings).render(); 11729 11730 return true; 11731 11732 case "mceAddFrameControl": 11733 w = v.window; 11734 11735 // Add tinyMCE global instance and tinymce namespace to specified window 11736 w.tinyMCE = tinyMCE; 11737 w.tinymce = tinymce; 11738 11739 tinymce.DOM.doc = w.document; 11740 tinymce.DOM.win = w; 11741 11742 ed = new tinymce.Editor(v.element_id, v); 11743 ed.render(); 11744 11745 // Fix IE memory leaks 11746 if (tinymce.isIE) { 11747 w.attachEvent('onunload', clr); 11748 } 11749 11750 v.page_window = null; 11751 11752 return true; 11753 11754 case "mceRemoveEditor": 11755 case "mceRemoveControl": 11756 if (ed) 11757 ed.remove(); 11758 11759 return true; 11760 11761 case 'mceToggleEditor': 11762 if (!ed) { 11763 t.execCommand('mceAddControl', 0, v); 11764 return true; 11765 } 11766 11767 if (ed.isHidden()) 11768 ed.show(); 11769 else 11770 ed.hide(); 11771 11772 return true; 11773 } 11774 11775 // Run command on active editor 11776 if (t.activeEditor) 11777 return t.activeEditor.execCommand(c, u, v); 11778 11779 return false; 11780 }, 11781 11782 execInstanceCommand : function(id, c, u, v) { 11783 var ed = this.get(id); 11784 11785 if (ed) 11786 return ed.execCommand(c, u, v); 11787 11788 return false; 11789 }, 11790 11791 triggerSave : function() { 11792 each(this.editors, function(e) { 11793 e.save(); 11794 }); 11795 }, 11796 11797 addI18n : function(p, o) { 11798 var lo, i18n = this.i18n; 11799 11800 if (!tinymce.is(p, 'string')) { 11801 each(p, function(o, lc) { 11802 each(o, function(o, g) { 11803 each(o, function(o, k) { 11804 if (g === 'common') 11805 i18n[lc + '.' + k] = o; 11806 else 11807 i18n[lc + '.' + g + '.' + k] = o; 11808 }); 11809 }); 11810 }); 11811 } else { 11812 each(o, function(o, k) { 11813 i18n[p + '.' + k] = o; 11814 }); 11815 } 11816 }, 11817 11818 // Private methods 11819 11820 _setActive : function(editor) { 11821 this.selectedInstance = this.activeEditor = editor; 11822 } 11823 }); 11824 })(tinymce); 11825 11826 (function(tinymce) { 11827 // Shorten these names 11828 var DOM = tinymce.DOM, Event = tinymce.dom.Event, extend = tinymce.extend, 11829 each = tinymce.each, isGecko = tinymce.isGecko, 11830 isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, is = tinymce.is, 11831 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, 11832 explode = tinymce.explode; 11833 11834 tinymce.create('tinymce.Editor', { 11835 Editor : function(id, settings) { 11836 var self = this, TRUE = true; 11837 11838 self.settings = settings = extend({ 11839 id : id, 11840 language : 'en', 11841 theme : 'advanced', 11842 skin : 'default', 11843 delta_width : 0, 11844 delta_height : 0, 11845 popup_css : '', 11846 plugins : '', 11847 document_base_url : tinymce.documentBaseURL, 11848 add_form_submit_trigger : TRUE, 11849 submit_patch : TRUE, 11850 add_unload_trigger : TRUE, 11851 convert_urls : TRUE, 11852 relative_urls : TRUE, 11853 remove_script_host : TRUE, 11854 table_inline_editing : false, 11855 object_resizing : TRUE, 11856 accessibility_focus : TRUE, 11857 doctype : tinymce.isIE6 ? '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">' : '<!DOCTYPE>', // Use old doctype on IE 6 to avoid horizontal scroll 11858 visual : TRUE, 11859 font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large', 11860 font_size_legacy_values : 'xx-small,small,medium,large,x-large,xx-large,300%', // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size 11861 apply_source_formatting : TRUE, 11862 directionality : 'ltr', 11863 forced_root_block : 'p', 11864 hidden_input : TRUE, 11865 padd_empty_editor : TRUE, 11866 render_ui : TRUE, 11867 indentation : '30px', 11868 fix_table_elements : TRUE, 11869 inline_styles : TRUE, 11870 convert_fonts_to_spans : TRUE, 11871 indent : 'simple', 11872 indent_before : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 11873 indent_after : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 11874 validate : TRUE, 11875 entity_encoding : 'named', 11876 url_converter : self.convertURL, 11877 url_converter_scope : self, 11878 ie7_compat : TRUE 11879 }, settings); 11880 11881 self.id = self.editorId = id; 11882 11883 self.isNotDirty = false; 11884 11885 self.plugins = {}; 11886 11887 self.documentBaseURI = new tinymce.util.URI(settings.document_base_url || tinymce.documentBaseURL, { 11888 base_uri : tinyMCE.baseURI 11889 }); 11890 11891 self.baseURI = tinymce.baseURI; 11892 11893 self.contentCSS = []; 11894 11895 self.contentStyles = []; 11896 11897 // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic 11898 self.setupEvents(); 11899 11900 // Internal command handler objects 11901 self.execCommands = {}; 11902 self.queryStateCommands = {}; 11903 self.queryValueCommands = {}; 11904 11905 // Call setup 11906 self.execCallback('setup', self); 11907 }, 11908 11909 render : function(nst) { 11910 var t = this, s = t.settings, id = t.id, sl = tinymce.ScriptLoader; 11911 11912 // Page is not loaded yet, wait for it 11913 if (!Event.domLoaded) { 11914 Event.add(window, 'ready', function() { 11915 t.render(); 11916 }); 11917 return; 11918 } 11919 11920 tinyMCE.settings = s; 11921 11922 // Element not found, then skip initialization 11923 if (!t.getElement()) 11924 return; 11925 11926 // Is a iPad/iPhone and not on iOS5, then skip initialization. We need to sniff 11927 // here since the browser says it has contentEditable support but there is no visible caret. 11928 if (tinymce.isIDevice && !tinymce.isIOS5) 11929 return; 11930 11931 // Add hidden input for non input elements inside form elements 11932 if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form')) 11933 DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id); 11934 11935 // Hide target element early to prevent content flashing 11936 if (!s.content_editable) { 11937 t.orgVisibility = t.getElement().style.visibility; 11938 t.getElement().style.visibility = 'hidden'; 11939 } 11940 11941 if (tinymce.WindowManager) 11942 t.windowManager = new tinymce.WindowManager(t); 11943 11944 if (s.encoding == 'xml') { 11945 t.onGetContent.add(function(ed, o) { 11946 if (o.save) 11947 o.content = DOM.encode(o.content); 11948 }); 11949 } 11950 11951 if (s.add_form_submit_trigger) { 11952 t.onSubmit.addToTop(function() { 11953 if (t.initialized) { 11954 t.save(); 11955 t.isNotDirty = 1; 11956 } 11957 }); 11958 } 11959 11960 if (s.add_unload_trigger) { 11961 t._beforeUnload = tinyMCE.onBeforeUnload.add(function() { 11962 if (t.initialized && !t.destroyed && !t.isHidden()) 11963 t.save({format : 'raw', no_events : true}); 11964 }); 11965 } 11966 11967 tinymce.addUnload(t.destroy, t); 11968 11969 if (s.submit_patch) { 11970 t.onBeforeRenderUI.add(function() { 11971 var n = t.getElement().form; 11972 11973 if (!n) 11974 return; 11975 11976 // Already patched 11977 if (n._mceOldSubmit) 11978 return; 11979 11980 // Check page uses id="submit" or name="submit" for it's submit button 11981 if (!n.submit.nodeType && !n.submit.length) { 11982 t.formElement = n; 11983 n._mceOldSubmit = n.submit; 11984 n.submit = function() { 11985 // Save all instances 11986 tinymce.triggerSave(); 11987 t.isNotDirty = 1; 11988 11989 return t.formElement._mceOldSubmit(t.formElement); 11990 }; 11991 } 11992 11993 n = null; 11994 }); 11995 } 11996 11997 // Load scripts 11998 function loadScripts() { 11999 if (s.language && s.language_load !== false) 12000 sl.add(tinymce.baseURL + '/langs/' + s.language + '.js'); 12001 12002 if (s.theme && typeof s.theme != "function" && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme]) 12003 ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js'); 12004 12005 each(explode(s.plugins), function(p) { 12006 if (p &&!PluginManager.urls[p]) { 12007 if (p.charAt(0) == '-') { 12008 p = p.substr(1, p.length); 12009 var dependencies = PluginManager.dependencies(p); 12010 each(dependencies, function(dep) { 12011 var defaultSettings = {prefix:'plugins/', resource: dep, suffix:'/editor_plugin' + tinymce.suffix + '.js'}; 12012 dep = PluginManager.createUrl(defaultSettings, dep); 12013 PluginManager.load(dep.resource, dep); 12014 }); 12015 } else { 12016 // Skip safari plugin, since it is removed as of 3.3b1 12017 if (p == 'safari') { 12018 return; 12019 } 12020 PluginManager.load(p, {prefix:'plugins/', resource: p, suffix:'/editor_plugin' + tinymce.suffix + '.js'}); 12021 } 12022 } 12023 }); 12024 12025 // Init when que is loaded 12026 sl.loadQueue(function() { 12027 if (!t.removed) 12028 t.init(); 12029 }); 12030 }; 12031 12032 loadScripts(); 12033 }, 12034 12035 init : function() { 12036 var n, t = this, s = t.settings, w, h, mh, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = []; 12037 12038 tinymce.add(t); 12039 12040 s.aria_label = s.aria_label || DOM.getAttrib(e, 'aria-label', t.getLang('aria.rich_text_area')); 12041 12042 if (s.theme) { 12043 if (typeof s.theme != "function") { 12044 s.theme = s.theme.replace(/-/, ''); 12045 o = ThemeManager.get(s.theme); 12046 t.theme = new o(); 12047 12048 if (t.theme.init) 12049 t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, '')); 12050 } else { 12051 t.theme = s.theme; 12052 } 12053 } 12054 12055 function initPlugin(p) { 12056 var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po; 12057 if (c && tinymce.inArray(initializedPlugins,p) === -1) { 12058 each(PluginManager.dependencies(p), function(dep){ 12059 initPlugin(dep); 12060 }); 12061 po = new c(t, u); 12062 12063 t.plugins[p] = po; 12064 12065 if (po.init) { 12066 po.init(t, u); 12067 initializedPlugins.push(p); 12068 } 12069 } 12070 } 12071 12072 // Create all plugins 12073 each(explode(s.plugins.replace(/\-/g, '')), initPlugin); 12074 12075 // Setup popup CSS path(s) 12076 if (s.popup_css !== false) { 12077 if (s.popup_css) 12078 s.popup_css = t.documentBaseURI.toAbsolute(s.popup_css); 12079 else 12080 s.popup_css = t.baseURI.toAbsolute("themes/" + s.theme + "/skins/" + s.skin + "/dialog.css"); 12081 } 12082 12083 if (s.popup_css_add) 12084 s.popup_css += ',' + t.documentBaseURI.toAbsolute(s.popup_css_add); 12085 12086 t.controlManager = new tinymce.ControlManager(t); 12087 12088 // Enables users to override the control factory 12089 t.onBeforeRenderUI.dispatch(t, t.controlManager); 12090 12091 // Measure box 12092 if (s.render_ui && t.theme) { 12093 t.orgDisplay = e.style.display; 12094 12095 if (typeof s.theme != "function") { 12096 w = s.width || e.style.width || e.offsetWidth; 12097 h = s.height || e.style.height || e.offsetHeight; 12098 mh = s.min_height || 100; 12099 re = /^[0-9\.]+(|px)$/i; 12100 12101 if (re.test('' + w)) 12102 w = Math.max(parseInt(w, 10) + (o.deltaWidth || 0), 100); 12103 12104 if (re.test('' + h)) 12105 h = Math.max(parseInt(h, 10) + (o.deltaHeight || 0), mh); 12106 12107 // Render UI 12108 o = t.theme.renderUI({ 12109 targetNode : e, 12110 width : w, 12111 height : h, 12112 deltaWidth : s.delta_width, 12113 deltaHeight : s.delta_height 12114 }); 12115 12116 // Resize editor 12117 DOM.setStyles(o.sizeContainer || o.editorContainer, { 12118 width : w, 12119 height : h 12120 }); 12121 12122 h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); 12123 if (h < mh) 12124 h = mh; 12125 } else { 12126 o = s.theme(t, e); 12127 12128 // Convert element type to id:s 12129 if (o.editorContainer.nodeType) { 12130 o.editorContainer = o.editorContainer.id = o.editorContainer.id || t.id + "_parent"; 12131 } 12132 12133 // Convert element type to id:s 12134 if (o.iframeContainer.nodeType) { 12135 o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || t.id + "_iframecontainer"; 12136 } 12137 12138 // Use specified iframe height or the targets offsetHeight 12139 h = o.iframeHeight || e.offsetHeight; 12140 12141 // Store away the selection when it's changed to it can be restored later with a editor.focus() call 12142 if (isIE) { 12143 t.onInit.add(function(ed) { 12144 ed.dom.bind(ed.getBody(), 'beforedeactivate keydown', function() { 12145 ed.lastIERng = ed.selection.getRng(); 12146 }); 12147 }); 12148 } 12149 } 12150 12151 t.editorContainer = o.editorContainer; 12152 } 12153 12154 // Load specified content CSS last 12155 if (s.content_css) { 12156 each(explode(s.content_css), function(u) { 12157 t.contentCSS.push(t.documentBaseURI.toAbsolute(u)); 12158 }); 12159 } 12160 12161 // Content editable mode ends here 12162 if (s.content_editable) { 12163 e = n = o = null; // Fix IE leak 12164 return t.initContentBody(); 12165 } 12166 12167 // User specified a document.domain value 12168 if (document.domain && location.hostname != document.domain) 12169 tinymce.relaxedDomain = document.domain; 12170 12171 t.iframeHTML = s.doctype + '<html><head xmlns="http://www.w3.org/1999/xhtml">'; 12172 12173 // We only need to override paths if we have to 12174 // IE has a bug where it remove site absolute urls to relative ones if this is specified 12175 if (s.document_base_url != tinymce.documentBaseURL) 12176 t.iframeHTML += '<base href="' + t.documentBaseURI.getURI() + '" />'; 12177 12178 // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode. 12179 if (s.ie7_compat) 12180 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />'; 12181 else 12182 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=edge" />'; 12183 12184 t.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'; 12185 12186 // Load the CSS by injecting them into the HTML this will reduce "flicker" 12187 for (i = 0; i < t.contentCSS.length; i++) { 12188 t.iframeHTML += '<link type="text/css" rel="stylesheet" href="' + t.contentCSS[i] + '" />'; 12189 } 12190 12191 t.contentCSS = []; 12192 12193 bi = s.body_id || 'tinymce'; 12194 if (bi.indexOf('=') != -1) { 12195 bi = t.getParam('body_id', '', 'hash'); 12196 bi = bi[t.id] || bi; 12197 } 12198 12199 bc = s.body_class || ''; 12200 if (bc.indexOf('=') != -1) { 12201 bc = t.getParam('body_class', '', 'hash'); 12202 bc = bc[t.id] || ''; 12203 } 12204 12205 t.iframeHTML += '</head><body id="' + bi + '" class="mceContentBody ' + bc + '" onload="window.parent.tinyMCE.get(\'' + t.id + '\').onLoad.dispatch();"><br></body></html>'; 12206 12207 // Domain relaxing enabled, then set document domain 12208 if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) { 12209 // We need to write the contents here in IE since multiple writes messes up refresh button and back button 12210 u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.initContentBody();})()'; 12211 } 12212 12213 // Create iframe 12214 // TODO: ACC add the appropriate description on this. 12215 n = DOM.add(o.iframeContainer, 'iframe', { 12216 id : t.id + "_ifr", 12217 src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7 12218 frameBorder : '0', 12219 allowTransparency : "true", 12220 title : s.aria_label, 12221 style : { 12222 width : '100%', 12223 height : h, 12224 display : 'block' // Important for Gecko to render the iframe correctly 12225 } 12226 }); 12227 12228 t.contentAreaContainer = o.iframeContainer; 12229 12230 if (o.editorContainer) { 12231 DOM.get(o.editorContainer).style.display = t.orgDisplay; 12232 } 12233 12234 // Restore visibility on target element 12235 e.style.visibility = t.orgVisibility; 12236 12237 DOM.get(t.id).style.display = 'none'; 12238 DOM.setAttrib(t.id, 'aria-hidden', true); 12239 12240 if (!tinymce.relaxedDomain || !u) 12241 t.initContentBody(); 12242 12243 e = n = o = null; // Cleanup 12244 }, 12245 12246 initContentBody : function() { 12247 var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), html, body, contentCssText; 12248 12249 // Setup iframe body 12250 if ((!isIE || !tinymce.relaxedDomain) && !settings.content_editable) { 12251 doc.open(); 12252 doc.write(self.iframeHTML); 12253 doc.close(); 12254 12255 if (tinymce.relaxedDomain) 12256 doc.domain = tinymce.relaxedDomain; 12257 } 12258 12259 if (settings.content_editable) { 12260 DOM.addClass(targetElm, 'mceContentBody'); 12261 self.contentDocument = doc = settings.content_document || document; 12262 self.contentWindow = settings.content_window || window; 12263 self.bodyElement = targetElm; 12264 12265 // Prevent leak in IE 12266 settings.content_document = settings.content_window = null; 12267 } 12268 12269 // It will not steal focus while setting contentEditable 12270 body = self.getBody(); 12271 body.disabled = true; 12272 12273 if (!settings.readonly) 12274 body.contentEditable = self.getParam('content_editable_state', true); 12275 12276 body.disabled = false; 12277 12278 self.schema = new tinymce.html.Schema(settings); 12279 12280 self.dom = new tinymce.dom.DOMUtils(doc, { 12281 keep_values : true, 12282 url_converter : self.convertURL, 12283 url_converter_scope : self, 12284 hex_colors : settings.force_hex_style_colors, 12285 class_filter : settings.class_filter, 12286 update_styles : true, 12287 root_element : settings.content_editable ? self.id : null, 12288 schema : self.schema 12289 }); 12290 12291 self.parser = new tinymce.html.DomParser(settings, self.schema); 12292 12293 // Convert src and href into data-mce-src, data-mce-href and data-mce-style 12294 self.parser.addAttributeFilter('src,href,style', function(nodes, name) { 12295 var i = nodes.length, node, dom = self.dom, value, internalName; 12296 12297 while (i--) { 12298 node = nodes[i]; 12299 value = node.attr(name); 12300 internalName = 'data-mce-' + name; 12301 12302 // Add internal attribute if we need to we don't on a refresh of the document 12303 if (!node.attributes.map[internalName]) { 12304 if (name === "style") 12305 node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name)); 12306 else 12307 node.attr(internalName, self.convertURL(value, name, node.name)); 12308 } 12309 } 12310 }); 12311 12312 // Keep scripts from executing 12313 self.parser.addNodeFilter('script', function(nodes, name) { 12314 var i = nodes.length, node; 12315 12316 while (i--) { 12317 node = nodes[i]; 12318 node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript')); 12319 } 12320 }); 12321 12322 self.parser.addNodeFilter('#cdata', function(nodes, name) { 12323 var i = nodes.length, node; 12324 12325 while (i--) { 12326 node = nodes[i]; 12327 node.type = 8; 12328 node.name = '#comment'; 12329 node.value = '[CDATA[' + node.value + ']]'; 12330 } 12331 }); 12332 12333 self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) { 12334 var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements(); 12335 12336 while (i--) { 12337 node = nodes[i]; 12338 12339 if (node.isEmpty(nonEmptyElements)) 12340 node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true; 12341 } 12342 }); 12343 12344 self.serializer = new tinymce.dom.Serializer(settings, self.dom, self.schema); 12345 12346 self.selection = new tinymce.dom.Selection(self.dom, self.getWin(), self.serializer, self); 12347 12348 self.formatter = new tinymce.Formatter(self); 12349 12350 self.undoManager = new tinymce.UndoManager(self); 12351 12352 self.forceBlocks = new tinymce.ForceBlocks(self); 12353 self.enterKey = new tinymce.EnterKey(self); 12354 self.editorCommands = new tinymce.EditorCommands(self); 12355 12356 self.onExecCommand.add(function(editor, command) { 12357 // Don't refresh the select lists until caret move 12358 if (!/^(FontName|FontSize)$/.test(command)) 12359 self.nodeChanged(); 12360 }); 12361 12362 // Pass through 12363 self.serializer.onPreProcess.add(function(se, o) { 12364 return self.onPreProcess.dispatch(self, o, se); 12365 }); 12366 12367 self.serializer.onPostProcess.add(function(se, o) { 12368 return self.onPostProcess.dispatch(self, o, se); 12369 }); 12370 12371 self.onPreInit.dispatch(self); 12372 12373 if (!settings.browser_spellcheck && !settings.gecko_spellcheck) 12374 doc.body.spellcheck = false; 12375 12376 if (!settings.readonly) { 12377 self.bindNativeEvents(); 12378 } 12379 12380 self.controlManager.onPostRender.dispatch(self, self.controlManager); 12381 self.onPostRender.dispatch(self); 12382 12383 self.quirks = tinymce.util.Quirks(self); 12384 12385 if (settings.directionality) 12386 body.dir = settings.directionality; 12387 12388 if (settings.nowrap) 12389 body.style.whiteSpace = "nowrap"; 12390 12391 if (settings.protect) { 12392 self.onBeforeSetContent.add(function(ed, o) { 12393 each(settings.protect, function(pattern) { 12394 o.content = o.content.replace(pattern, function(str) { 12395 return '<!--mce:protected ' + escape(str) + '-->'; 12396 }); 12397 }); 12398 }); 12399 } 12400 12401 // Add visual aids when new contents is added 12402 self.onSetContent.add(function() { 12403 self.addVisual(self.getBody()); 12404 }); 12405 12406 // Remove empty contents 12407 if (settings.padd_empty_editor) { 12408 self.onPostProcess.add(function(ed, o) { 12409 o.content = o.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, ''); 12410 }); 12411 } 12412 12413 self.load({initial : true, format : 'html'}); 12414 self.startContent = self.getContent({format : 'raw'}); 12415 12416 self.initialized = true; 12417 12418 self.onInit.dispatch(self); 12419 self.execCallback('setupcontent_callback', self.id, body, doc); 12420 self.execCallback('init_instance_callback', self); 12421 self.focus(true); 12422 self.nodeChanged({initial : true}); 12423 12424 // Add editor specific CSS styles 12425 if (self.contentStyles.length > 0) { 12426 contentCssText = ''; 12427 12428 each(self.contentStyles, function(style) { 12429 contentCssText += style + "\r\n"; 12430 }); 12431 12432 self.dom.addStyle(contentCssText); 12433 } 12434 12435 // Load specified content CSS last 12436 each(self.contentCSS, function(url) { 12437 self.dom.loadCSS(url); 12438 }); 12439 12440 // Handle auto focus 12441 if (settings.auto_focus) { 12442 setTimeout(function () { 12443 var ed = tinymce.get(settings.auto_focus); 12444 12445 ed.selection.select(ed.getBody(), 1); 12446 ed.selection.collapse(1); 12447 ed.getBody().focus(); 12448 ed.getWin().focus(); 12449 }, 100); 12450 } 12451 12452 // Clean up references for IE 12453 targetElm = doc = body = null; 12454 }, 12455 12456 focus : function(skip_focus) { 12457 var oed, self = this, selection = self.selection, contentEditable = self.settings.content_editable, ieRng, controlElm, doc = self.getDoc(), body; 12458 12459 if (!skip_focus) { 12460 if (self.lastIERng) { 12461 selection.setRng(self.lastIERng); 12462 } 12463 12464 // Get selected control element 12465 ieRng = selection.getRng(); 12466 if (ieRng.item) { 12467 controlElm = ieRng.item(0); 12468 } 12469 12470 self._refreshContentEditable(); 12471 12472 // Focus the window iframe 12473 if (!contentEditable) { 12474 self.getWin().focus(); 12475 } 12476 12477 // Focus the body as well since it's contentEditable 12478 if (tinymce.isGecko || contentEditable) { 12479 body = self.getBody(); 12480 12481 // Check for setActive since it doesn't scroll to the element 12482 if (body.setActive) { 12483 body.setActive(); 12484 } else { 12485 body.focus(); 12486 } 12487 12488 if (contentEditable) { 12489 selection.normalize(); 12490 } 12491 } 12492 12493 // Restore selected control element 12494 // This is needed when for example an image is selected within a 12495 // layer a call to focus will then remove the control selection 12496 if (controlElm && controlElm.ownerDocument == doc) { 12497 ieRng = doc.body.createControlRange(); 12498 ieRng.addElement(controlElm); 12499 ieRng.select(); 12500 } 12501 } 12502 12503 if (tinymce.activeEditor != self) { 12504 if ((oed = tinymce.activeEditor) != null) 12505 oed.onDeactivate.dispatch(oed, self); 12506 12507 self.onActivate.dispatch(self, oed); 12508 } 12509 12510 tinymce._setActive(self); 12511 }, 12512 12513 execCallback : function(n) { 12514 var t = this, f = t.settings[n], s; 12515 12516 if (!f) 12517 return; 12518 12519 // Look through lookup 12520 if (t.callbackLookup && (s = t.callbackLookup[n])) { 12521 f = s.func; 12522 s = s.scope; 12523 } 12524 12525 if (is(f, 'string')) { 12526 s = f.replace(/\.\w+$/, ''); 12527 s = s ? tinymce.resolve(s) : 0; 12528 f = tinymce.resolve(f); 12529 t.callbackLookup = t.callbackLookup || {}; 12530 t.callbackLookup[n] = {func : f, scope : s}; 12531 } 12532 12533 return f.apply(s || t, Array.prototype.slice.call(arguments, 1)); 12534 }, 12535 12536 translate : function(s) { 12537 var c = this.settings.language || 'en', i18n = tinymce.i18n; 12538 12539 if (!s) 12540 return ''; 12541 12542 return i18n[c + '.' + s] || s.replace(/\{\#([^\}]+)\}/g, function(a, b) { 12543 return i18n[c + '.' + b] || '{#' + b + '}'; 12544 }); 12545 }, 12546 12547 getLang : function(n, dv) { 12548 return tinymce.i18n[(this.settings.language || 'en') + '.' + n] || (is(dv) ? dv : '{#' + n + '}'); 12549 }, 12550 12551 getParam : function(n, dv, ty) { 12552 var tr = tinymce.trim, v = is(this.settings[n]) ? this.settings[n] : dv, o; 12553 12554 if (ty === 'hash') { 12555 o = {}; 12556 12557 if (is(v, 'string')) { 12558 each(v.indexOf('=') > 0 ? v.split(/[;,](?![^=;,]*(?:[;,]|$))/) : v.split(','), function(v) { 12559 v = v.split('='); 12560 12561 if (v.length > 1) 12562 o[tr(v[0])] = tr(v[1]); 12563 else 12564 o[tr(v[0])] = tr(v); 12565 }); 12566 } else 12567 o = v; 12568 12569 return o; 12570 } 12571 12572 return v; 12573 }, 12574 12575 nodeChanged : function(o) { 12576 var self = this, selection = self.selection, node; 12577 12578 // Fix for bug #1896577 it seems that this can not be fired while the editor is loading 12579 if (self.initialized) { 12580 o = o || {}; 12581 12582 // Get start node 12583 node = selection.getStart() || self.getBody(); 12584 node = isIE && node.ownerDocument != self.getDoc() ? self.getBody() : node; // Fix for IE initial state 12585 12586 // Get parents and add them to object 12587 o.parents = []; 12588 self.dom.getParent(node, function(node) { 12589 if (node.nodeName == 'BODY') 12590 return true; 12591 12592 o.parents.push(node); 12593 }); 12594 12595 self.onNodeChange.dispatch( 12596 self, 12597 o ? o.controlManager || self.controlManager : self.controlManager, 12598 node, 12599 selection.isCollapsed(), 12600 o 12601 ); 12602 } 12603 }, 12604 12605 addButton : function(name, settings) { 12606 var self = this; 12607 12608 self.buttons = self.buttons || {}; 12609 self.buttons[name] = settings; 12610 }, 12611 12612 addCommand : function(name, callback, scope) { 12613 this.execCommands[name] = {func : callback, scope : scope || this}; 12614 }, 12615 12616 addQueryStateHandler : function(name, callback, scope) { 12617 this.queryStateCommands[name] = {func : callback, scope : scope || this}; 12618 }, 12619 12620 addQueryValueHandler : function(name, callback, scope) { 12621 this.queryValueCommands[name] = {func : callback, scope : scope || this}; 12622 }, 12623 12624 addShortcut : function(pa, desc, cmd_func, sc) { 12625 var t = this, c; 12626 12627 if (t.settings.custom_shortcuts === false) 12628 return false; 12629 12630 t.shortcuts = t.shortcuts || {}; 12631 12632 if (is(cmd_func, 'string')) { 12633 c = cmd_func; 12634 12635 cmd_func = function() { 12636 t.execCommand(c, false, null); 12637 }; 12638 } 12639 12640 if (is(cmd_func, 'object')) { 12641 c = cmd_func; 12642 12643 cmd_func = function() { 12644 t.execCommand(c[0], c[1], c[2]); 12645 }; 12646 } 12647 12648 each(explode(pa), function(pa) { 12649 var o = { 12650 func : cmd_func, 12651 scope : sc || this, 12652 desc : t.translate(desc), 12653 alt : false, 12654 ctrl : false, 12655 shift : false 12656 }; 12657 12658 each(explode(pa, '+'), function(v) { 12659 switch (v) { 12660 case 'alt': 12661 case 'ctrl': 12662 case 'shift': 12663 o[v] = true; 12664 break; 12665 12666 default: 12667 o.charCode = v.charCodeAt(0); 12668 o.keyCode = v.toUpperCase().charCodeAt(0); 12669 } 12670 }); 12671 12672 t.shortcuts[(o.ctrl ? 'ctrl' : '') + ',' + (o.alt ? 'alt' : '') + ',' + (o.shift ? 'shift' : '') + ',' + o.keyCode] = o; 12673 }); 12674 12675 return true; 12676 }, 12677 12678 execCommand : function(cmd, ui, val, a) { 12679 var t = this, s = 0, o, st; 12680 12681 if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint|SelectAll)$/.test(cmd) && (!a || !a.skip_focus)) 12682 t.focus(); 12683 12684 a = extend({}, a); 12685 t.onBeforeExecCommand.dispatch(t, cmd, ui, val, a); 12686 if (a.terminate) 12687 return false; 12688 12689 // Command callback 12690 if (t.execCallback('execcommand_callback', t.id, t.selection.getNode(), cmd, ui, val)) { 12691 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12692 return true; 12693 } 12694 12695 // Registred commands 12696 if (o = t.execCommands[cmd]) { 12697 st = o.func.call(o.scope, ui, val); 12698 12699 // Fall through on true 12700 if (st !== true) { 12701 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12702 return st; 12703 } 12704 } 12705 12706 // Plugin commands 12707 each(t.plugins, function(p) { 12708 if (p.execCommand && p.execCommand(cmd, ui, val)) { 12709 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12710 s = 1; 12711 return false; 12712 } 12713 }); 12714 12715 if (s) 12716 return true; 12717 12718 // Theme commands 12719 if (t.theme && t.theme.execCommand && t.theme.execCommand(cmd, ui, val)) { 12720 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12721 return true; 12722 } 12723 12724 // Editor commands 12725 if (t.editorCommands.execCommand(cmd, ui, val)) { 12726 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12727 return true; 12728 } 12729 12730 // Browser commands 12731 t.getDoc().execCommand(cmd, ui, val); 12732 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12733 }, 12734 12735 queryCommandState : function(cmd) { 12736 var t = this, o, s; 12737 12738 // Is hidden then return undefined 12739 if (t._isHidden()) 12740 return; 12741 12742 // Registred commands 12743 if (o = t.queryStateCommands[cmd]) { 12744 s = o.func.call(o.scope); 12745 12746 // Fall though on true 12747 if (s !== true) 12748 return s; 12749 } 12750 12751 // Registred commands 12752 o = t.editorCommands.queryCommandState(cmd); 12753 if (o !== -1) 12754 return o; 12755 12756 // Browser commands 12757 try { 12758 return this.getDoc().queryCommandState(cmd); 12759 } catch (ex) { 12760 // Fails sometimes see bug: 1896577 12761 } 12762 }, 12763 12764 queryCommandValue : function(c) { 12765 var t = this, o, s; 12766 12767 // Is hidden then return undefined 12768 if (t._isHidden()) 12769 return; 12770 12771 // Registred commands 12772 if (o = t.queryValueCommands[c]) { 12773 s = o.func.call(o.scope); 12774 12775 // Fall though on true 12776 if (s !== true) 12777 return s; 12778 } 12779 12780 // Registred commands 12781 o = t.editorCommands.queryCommandValue(c); 12782 if (is(o)) 12783 return o; 12784 12785 // Browser commands 12786 try { 12787 return this.getDoc().queryCommandValue(c); 12788 } catch (ex) { 12789 // Fails sometimes see bug: 1896577 12790 } 12791 }, 12792 12793 show : function() { 12794 var self = this; 12795 12796 DOM.show(self.getContainer()); 12797 DOM.hide(self.id); 12798 self.load(); 12799 }, 12800 12801 hide : function() { 12802 var self = this, doc = self.getDoc(); 12803 12804 // Fixed bug where IE has a blinking cursor left from the editor 12805 if (isIE && doc) 12806 doc.execCommand('SelectAll'); 12807 12808 // We must save before we hide so Safari doesn't crash 12809 self.save(); 12810 DOM.hide(self.getContainer()); 12811 DOM.setStyle(self.id, 'display', self.orgDisplay); 12812 }, 12813 12814 isHidden : function() { 12815 return !DOM.isHidden(this.id); 12816 }, 12817 12818 setProgressState : function(b, ti, o) { 12819 this.onSetProgressState.dispatch(this, b, ti, o); 12820 12821 return b; 12822 }, 12823 12824 load : function(o) { 12825 var t = this, e = t.getElement(), h; 12826 12827 if (e) { 12828 o = o || {}; 12829 o.load = true; 12830 12831 // Double encode existing entities in the value 12832 h = t.setContent(is(e.value) ? e.value : e.innerHTML, o); 12833 o.element = e; 12834 12835 if (!o.no_events) 12836 t.onLoadContent.dispatch(t, o); 12837 12838 o.element = e = null; 12839 12840 return h; 12841 } 12842 }, 12843 12844 save : function(o) { 12845 var t = this, e = t.getElement(), h, f; 12846 12847 if (!e || !t.initialized) 12848 return; 12849 12850 o = o || {}; 12851 o.save = true; 12852 12853 o.element = e; 12854 h = o.content = t.getContent(o); 12855 12856 if (!o.no_events) 12857 t.onSaveContent.dispatch(t, o); 12858 12859 h = o.content; 12860 12861 if (!/TEXTAREA|INPUT/i.test(e.nodeName)) { 12862 e.innerHTML = h; 12863 12864 // Update hidden form element 12865 if (f = DOM.getParent(t.id, 'form')) { 12866 each(f.elements, function(e) { 12867 if (e.name == t.id) { 12868 e.value = h; 12869 return false; 12870 } 12871 }); 12872 } 12873 } else 12874 e.value = h; 12875 12876 o.element = e = null; 12877 12878 return h; 12879 }, 12880 12881 setContent : function(content, args) { 12882 var self = this, rootNode, body = self.getBody(), forcedRootBlockName; 12883 12884 // Setup args object 12885 args = args || {}; 12886 args.format = args.format || 'html'; 12887 args.set = true; 12888 args.content = content; 12889 12890 // Do preprocessing 12891 if (!args.no_events) 12892 self.onBeforeSetContent.dispatch(self, args); 12893 12894 content = args.content; 12895 12896 // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content 12897 // It will also be impossible to place the caret in the editor unless there is a BR element present 12898 if (!tinymce.isIE && (content.length === 0 || /^\s+$/.test(content))) { 12899 forcedRootBlockName = self.settings.forced_root_block; 12900 if (forcedRootBlockName) 12901 content = '<' + forcedRootBlockName + '><br data-mce-bogus="1"></' + forcedRootBlockName + '>'; 12902 else 12903 content = '<br data-mce-bogus="1">'; 12904 12905 body.innerHTML = content; 12906 self.selection.select(body, true); 12907 self.selection.collapse(true); 12908 return; 12909 } 12910 12911 // Parse and serialize the html 12912 if (args.format !== 'raw') { 12913 content = new tinymce.html.Serializer({}, self.schema).serialize( 12914 self.parser.parse(content) 12915 ); 12916 } 12917 12918 // Set the new cleaned contents to the editor 12919 args.content = tinymce.trim(content); 12920 self.dom.setHTML(body, args.content); 12921 12922 // Do post processing 12923 if (!args.no_events) 12924 self.onSetContent.dispatch(self, args); 12925 12926 // Don't normalize selection if the focused element isn't the body in content editable mode since it will steal focus otherwise 12927 if (!self.settings.content_editable || document.activeElement === self.getBody()) { 12928 self.selection.normalize(); 12929 } 12930 12931 return args.content; 12932 }, 12933 12934 getContent : function(args) { 12935 var self = this, content; 12936 12937 // Setup args object 12938 args = args || {}; 12939 args.format = args.format || 'html'; 12940 args.get = true; 12941 args.getInner = true; 12942 12943 // Do preprocessing 12944 if (!args.no_events) 12945 self.onBeforeGetContent.dispatch(self, args); 12946 12947 // Get raw contents or by default the cleaned contents 12948 if (args.format == 'raw') 12949 content = self.getBody().innerHTML; 12950 else 12951 content = self.serializer.serialize(self.getBody(), args); 12952 12953 args.content = tinymce.trim(content); 12954 12955 // Do post processing 12956 if (!args.no_events) 12957 self.onGetContent.dispatch(self, args); 12958 12959 return args.content; 12960 }, 12961 12962 isDirty : function() { 12963 var self = this; 12964 12965 return tinymce.trim(self.startContent) != tinymce.trim(self.getContent({format : 'raw', no_events : 1})) && !self.isNotDirty; 12966 }, 12967 12968 getContainer : function() { 12969 var self = this; 12970 12971 if (!self.container) 12972 self.container = DOM.get(self.editorContainer || self.id + '_parent'); 12973 12974 return self.container; 12975 }, 12976 12977 getContentAreaContainer : function() { 12978 return this.contentAreaContainer; 12979 }, 12980 12981 getElement : function() { 12982 return DOM.get(this.settings.content_element || this.id); 12983 }, 12984 12985 getWin : function() { 12986 var self = this, elm; 12987 12988 if (!self.contentWindow) { 12989 elm = DOM.get(self.id + "_ifr"); 12990 12991 if (elm) 12992 self.contentWindow = elm.contentWindow; 12993 } 12994 12995 return self.contentWindow; 12996 }, 12997 12998 getDoc : function() { 12999 var self = this, win; 13000 13001 if (!self.contentDocument) { 13002 win = self.getWin(); 13003 13004 if (win) 13005 self.contentDocument = win.document; 13006 } 13007 13008 return self.contentDocument; 13009 }, 13010 13011 getBody : function() { 13012 return this.bodyElement || this.getDoc().body; 13013 }, 13014 13015 convertURL : function(url, name, elm) { 13016 var self = this, settings = self.settings; 13017 13018 // Use callback instead 13019 if (settings.urlconverter_callback) 13020 return self.execCallback('urlconverter_callback', url, elm, true, name); 13021 13022 // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs 13023 if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0) 13024 return url; 13025 13026 // Convert to relative 13027 if (settings.relative_urls) 13028 return self.documentBaseURI.toRelative(url); 13029 13030 // Convert to absolute 13031 url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host); 13032 13033 return url; 13034 }, 13035 13036 addVisual : function(elm) { 13037 var self = this, settings = self.settings, dom = self.dom, cls; 13038 13039 elm = elm || self.getBody(); 13040 13041 if (!is(self.hasVisual)) 13042 self.hasVisual = settings.visual; 13043 13044 each(dom.select('table,a', elm), function(elm) { 13045 var value; 13046 13047 switch (elm.nodeName) { 13048 case 'TABLE': 13049 cls = settings.visual_table_class || 'mceItemTable'; 13050 value = dom.getAttrib(elm, 'border'); 13051 13052 if (!value || value == '0') { 13053 if (self.hasVisual) 13054 dom.addClass(elm, cls); 13055 else 13056 dom.removeClass(elm, cls); 13057 } 13058 13059 return; 13060 13061 case 'A': 13062 if (!dom.getAttrib(elm, 'href', false)) { 13063 value = dom.getAttrib(elm, 'name') || elm.id; 13064 cls = 'mceItemAnchor'; 13065 13066 if (value) { 13067 if (self.hasVisual) 13068 dom.addClass(elm, cls); 13069 else 13070 dom.removeClass(elm, cls); 13071 } 13072 } 13073 13074 return; 13075 } 13076 }); 13077 13078 self.onVisualAid.dispatch(self, elm, self.hasVisual); 13079 }, 13080 13081 remove : function() { 13082 var self = this, elm = self.getContainer(); 13083 13084 if (!self.removed) { 13085 self.removed = 1; // Cancels post remove event execution 13086 self.hide(); 13087 13088 // Don't clear the window or document if content editable 13089 // is enabled since other instances might still be present 13090 if (!self.settings.content_editable) { 13091 Event.unbind(self.getWin()); 13092 Event.unbind(self.getDoc()); 13093 } 13094 13095 Event.unbind(self.getBody()); 13096 Event.clear(elm); 13097 13098 self.execCallback('remove_instance_callback', self); 13099 self.onRemove.dispatch(self); 13100 13101 // Clear all execCommand listeners this is required to avoid errors if the editor was removed inside another command 13102 self.onExecCommand.listeners = []; 13103 13104 tinymce.remove(self); 13105 DOM.remove(elm); 13106 } 13107 }, 13108 13109 destroy : function(s) { 13110 var t = this; 13111 13112 // One time is enough 13113 if (t.destroyed) 13114 return; 13115 13116 // We must unbind on Gecko since it would otherwise produce the pesky "attempt to run compile-and-go script on a cleared scope" message 13117 if (isGecko) { 13118 Event.unbind(t.getDoc()); 13119 Event.unbind(t.getWin()); 13120 Event.unbind(t.getBody()); 13121 } 13122 13123 if (!s) { 13124 tinymce.removeUnload(t.destroy); 13125 tinyMCE.onBeforeUnload.remove(t._beforeUnload); 13126 13127 // Manual destroy 13128 if (t.theme && t.theme.destroy) 13129 t.theme.destroy(); 13130 13131 // Destroy controls, selection and dom 13132 t.controlManager.destroy(); 13133 t.selection.destroy(); 13134 t.dom.destroy(); 13135 } 13136 13137 if (t.formElement) { 13138 t.formElement.submit = t.formElement._mceOldSubmit; 13139 t.formElement._mceOldSubmit = null; 13140 } 13141 13142 t.contentAreaContainer = t.formElement = t.container = t.settings.content_element = t.bodyElement = t.contentDocument = t.contentWindow = null; 13143 13144 if (t.selection) 13145 t.selection = t.selection.win = t.selection.dom = t.selection.dom.doc = null; 13146 13147 t.destroyed = 1; 13148 }, 13149 13150 // Internal functions 13151 13152 _refreshContentEditable : function() { 13153 var self = this, body, parent; 13154 13155 // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again 13156 if (self._isHidden()) { 13157 body = self.getBody(); 13158 parent = body.parentNode; 13159 13160 parent.removeChild(body); 13161 parent.appendChild(body); 13162 13163 body.focus(); 13164 } 13165 }, 13166 13167 _isHidden : function() { 13168 var s; 13169 13170 if (!isGecko) 13171 return 0; 13172 13173 // Weird, wheres that cursor selection? 13174 s = this.selection.getSel(); 13175 return (!s || !s.rangeCount || s.rangeCount === 0); 13176 } 13177 }); 13178 })(tinymce); 13179 (function(tinymce) { 13180 var each = tinymce.each; 13181 13182 tinymce.Editor.prototype.setupEvents = function() { 13183 var self = this, settings = self.settings; 13184 13185 // Add events to the editor 13186 each([ 13187 'onPreInit', 13188 13189 'onBeforeRenderUI', 13190 13191 'onPostRender', 13192 13193 'onLoad', 13194 13195 'onInit', 13196 13197 'onRemove', 13198 13199 'onActivate', 13200 13201 'onDeactivate', 13202 13203 'onClick', 13204 13205 'onEvent', 13206 13207 'onMouseUp', 13208 13209 'onMouseDown', 13210 13211 'onDblClick', 13212 13213 'onKeyDown', 13214 13215 'onKeyUp', 13216 13217 'onKeyPress', 13218 13219 'onContextMenu', 13220 13221 'onSubmit', 13222 13223 'onReset', 13224 13225 'onPaste', 13226 13227 'onPreProcess', 13228 13229 'onPostProcess', 13230 13231 'onBeforeSetContent', 13232 13233 'onBeforeGetContent', 13234 13235 'onSetContent', 13236 13237 'onGetContent', 13238 13239 'onLoadContent', 13240 13241 'onSaveContent', 13242 13243 'onNodeChange', 13244 13245 'onChange', 13246 13247 'onBeforeExecCommand', 13248 13249 'onExecCommand', 13250 13251 'onUndo', 13252 13253 'onRedo', 13254 13255 'onVisualAid', 13256 13257 'onSetProgressState', 13258 13259 'onSetAttrib' 13260 ], function(name) { 13261 self[name] = new tinymce.util.Dispatcher(self); 13262 }); 13263 13264 // Handle legacy cleanup_callback option 13265 if (settings.cleanup_callback) { 13266 self.onBeforeSetContent.add(function(ed, o) { 13267 o.content = ed.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); 13268 }); 13269 13270 self.onPreProcess.add(function(ed, o) { 13271 if (o.set) 13272 ed.execCallback('cleanup_callback', 'insert_to_editor_dom', o.node, o); 13273 13274 if (o.get) 13275 ed.execCallback('cleanup_callback', 'get_from_editor_dom', o.node, o); 13276 }); 13277 13278 self.onPostProcess.add(function(ed, o) { 13279 if (o.set) 13280 o.content = ed.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); 13281 13282 if (o.get) 13283 o.content = ed.execCallback('cleanup_callback', 'get_from_editor', o.content, o); 13284 }); 13285 } 13286 13287 // Handle legacy save_callback option 13288 if (settings.save_callback) { 13289 self.onGetContent.add(function(ed, o) { 13290 if (o.save) 13291 o.content = ed.execCallback('save_callback', ed.id, o.content, ed.getBody()); 13292 }); 13293 } 13294 13295 // Handle legacy handle_event_callback option 13296 if (settings.handle_event_callback) { 13297 self.onEvent.add(function(ed, e, o) { 13298 if (self.execCallback('handle_event_callback', e, ed, o) === false) { 13299 e.preventDefault(); 13300 e.stopPropagation(); 13301 } 13302 }); 13303 } 13304 13305 // Handle legacy handle_node_change_callback option 13306 if (settings.handle_node_change_callback) { 13307 self.onNodeChange.add(function(ed, cm, n) { 13308 ed.execCallback('handle_node_change_callback', ed.id, n, -1, -1, true, ed.selection.isCollapsed()); 13309 }); 13310 } 13311 13312 // Handle legacy save_callback option 13313 if (settings.save_callback) { 13314 self.onSaveContent.add(function(ed, o) { 13315 var h = ed.execCallback('save_callback', ed.id, o.content, ed.getBody()); 13316 13317 if (h) 13318 o.content = h; 13319 }); 13320 } 13321 13322 // Handle legacy onchange_callback option 13323 if (settings.onchange_callback) { 13324 self.onChange.add(function(ed, l) { 13325 ed.execCallback('onchange_callback', ed, l); 13326 }); 13327 } 13328 }; 13329 13330 tinymce.Editor.prototype.bindNativeEvents = function() { 13331 // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset 13332 var self = this, i, settings = self.settings, dom = self.dom, nativeToDispatcherMap; 13333 13334 nativeToDispatcherMap = { 13335 mouseup : 'onMouseUp', 13336 mousedown : 'onMouseDown', 13337 click : 'onClick', 13338 keyup : 'onKeyUp', 13339 keydown : 'onKeyDown', 13340 keypress : 'onKeyPress', 13341 submit : 'onSubmit', 13342 reset : 'onReset', 13343 contextmenu : 'onContextMenu', 13344 dblclick : 'onDblClick', 13345 paste : 'onPaste' // Doesn't work in all browsers yet 13346 }; 13347 13348 // Handler that takes a native event and sends it out to a dispatcher like onKeyDown 13349 function eventHandler(evt, args) { 13350 var type = evt.type; 13351 13352 // Don't fire events when it's removed 13353 if (self.removed) 13354 return; 13355 13356 // Sends the native event out to a global dispatcher then to the specific event dispatcher 13357 if (self.onEvent.dispatch(self, evt, args) !== false) { 13358 self[nativeToDispatcherMap[evt.fakeType || evt.type]].dispatch(self, evt, args); 13359 } 13360 }; 13361 13362 // Opera doesn't support focus event for contentEditable elements so we need to fake it 13363 function doOperaFocus(e) { 13364 self.focus(true); 13365 }; 13366 13367 function nodeChanged(ed, e) { 13368 // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything 13369 if (e.keyCode != 65 || !tinymce.VK.metaKeyPressed(e)) { 13370 self.selection.normalize(); 13371 } 13372 13373 self.nodeChanged(); 13374 } 13375 13376 // Add DOM events 13377 each(nativeToDispatcherMap, function(dispatcherName, nativeName) { 13378 var root = settings.content_editable ? self.getBody() : self.getDoc(); 13379 13380 switch (nativeName) { 13381 case 'contextmenu': 13382 dom.bind(root, nativeName, eventHandler); 13383 break; 13384 13385 case 'paste': 13386 dom.bind(self.getBody(), nativeName, eventHandler); 13387 break; 13388 13389 case 'submit': 13390 case 'reset': 13391 dom.bind(self.getElement().form || tinymce.DOM.getParent(self.id, 'form'), nativeName, eventHandler); 13392 break; 13393 13394 default: 13395 dom.bind(root, nativeName, eventHandler); 13396 } 13397 }); 13398 13399 // Set the editor as active when focused 13400 dom.bind(settings.content_editable ? self.getBody() : (tinymce.isGecko ? self.getDoc() : self.getWin()), 'focus', function(e) { 13401 self.focus(true); 13402 }); 13403 13404 if (settings.content_editable && tinymce.isOpera) { 13405 dom.bind(self.getBody(), 'click', doOperaFocus); 13406 dom.bind(self.getBody(), 'keydown', doOperaFocus); 13407 } 13408 13409 // Add node change handler 13410 self.onMouseUp.add(nodeChanged); 13411 13412 self.onKeyUp.add(function(ed, e) { 13413 var keyCode = e.keyCode; 13414 13415 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || keyCode == 46 || keyCode == 8 || (tinymce.isMac && (keyCode == 91 || keyCode == 93)) || e.ctrlKey) 13416 nodeChanged(ed, e); 13417 }); 13418 13419 // Add reset handler 13420 self.onReset.add(function() { 13421 self.setContent(self.startContent, {format : 'raw'}); 13422 }); 13423 13424 // Add shortcuts 13425 function handleShortcut(e, execute) { 13426 if (e.altKey || e.ctrlKey || e.metaKey) { 13427 each(self.shortcuts, function(shortcut) { 13428 var ctrlState = tinymce.isMac ? e.metaKey : e.ctrlKey; 13429 13430 if (shortcut.ctrl != ctrlState || shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) 13431 return; 13432 13433 if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) { 13434 e.preventDefault(); 13435 13436 if (execute) { 13437 shortcut.func.call(shortcut.scope); 13438 } 13439 13440 return true; 13441 } 13442 }); 13443 } 13444 }; 13445 13446 self.onKeyUp.add(function(ed, e) { 13447 handleShortcut(e); 13448 }); 13449 13450 self.onKeyPress.add(function(ed, e) { 13451 handleShortcut(e); 13452 }); 13453 13454 self.onKeyDown.add(function(ed, e) { 13455 handleShortcut(e, true); 13456 }); 13457 13458 if (tinymce.isOpera) { 13459 self.onClick.add(function(ed, e) { 13460 e.preventDefault(); 13461 }); 13462 } 13463 }; 13464 })(tinymce); 13465 (function(tinymce) { 13466 // Added for compression purposes 13467 var each = tinymce.each, undef, TRUE = true, FALSE = false; 13468 13469 tinymce.EditorCommands = function(editor) { 13470 var dom = editor.dom, 13471 selection = editor.selection, 13472 commands = {state: {}, exec : {}, value : {}}, 13473 settings = editor.settings, 13474 formatter = editor.formatter, 13475 bookmark; 13476 13477 function execCommand(command, ui, value) { 13478 var func; 13479 13480 command = command.toLowerCase(); 13481 if (func = commands.exec[command]) { 13482 func(command, ui, value); 13483 return TRUE; 13484 } 13485 13486 return FALSE; 13487 }; 13488 13489 function queryCommandState(command) { 13490 var func; 13491 13492 command = command.toLowerCase(); 13493 if (func = commands.state[command]) 13494 return func(command); 13495 13496 return -1; 13497 }; 13498 13499 function queryCommandValue(command) { 13500 var func; 13501 13502 command = command.toLowerCase(); 13503 if (func = commands.value[command]) 13504 return func(command); 13505 13506 return FALSE; 13507 }; 13508 13509 function addCommands(command_list, type) { 13510 type = type || 'exec'; 13511 13512 each(command_list, function(callback, command) { 13513 each(command.toLowerCase().split(','), function(command) { 13514 commands[type][command] = callback; 13515 }); 13516 }); 13517 }; 13518 13519 // Expose public methods 13520 tinymce.extend(this, { 13521 execCommand : execCommand, 13522 queryCommandState : queryCommandState, 13523 queryCommandValue : queryCommandValue, 13524 addCommands : addCommands 13525 }); 13526 13527 // Private methods 13528 13529 function execNativeCommand(command, ui, value) { 13530 if (ui === undef) 13531 ui = FALSE; 13532 13533 if (value === undef) 13534 value = null; 13535 13536 return editor.getDoc().execCommand(command, ui, value); 13537 }; 13538 13539 function isFormatMatch(name) { 13540 return formatter.match(name); 13541 }; 13542 13543 function toggleFormat(name, value) { 13544 formatter.toggle(name, value ? {value : value} : undef); 13545 }; 13546 13547 function storeSelection(type) { 13548 bookmark = selection.getBookmark(type); 13549 }; 13550 13551 function restoreSelection() { 13552 selection.moveToBookmark(bookmark); 13553 }; 13554 13555 // Add execCommand overrides 13556 addCommands({ 13557 // Ignore these, added for compatibility 13558 'mceResetDesignMode,mceBeginUndoLevel' : function() {}, 13559 13560 // Add undo manager logic 13561 'mceEndUndoLevel,mceAddUndoLevel' : function() { 13562 editor.undoManager.add(); 13563 }, 13564 13565 'Cut,Copy,Paste' : function(command) { 13566 var doc = editor.getDoc(), failed; 13567 13568 // Try executing the native command 13569 try { 13570 execNativeCommand(command); 13571 } catch (ex) { 13572 // Command failed 13573 failed = TRUE; 13574 } 13575 13576 // Present alert message about clipboard access not being available 13577 if (failed || !doc.queryCommandSupported(command)) { 13578 if (tinymce.isGecko) { 13579 editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) { 13580 if (state) 13581 open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank'); 13582 }); 13583 } else 13584 editor.windowManager.alert(editor.getLang('clipboard_no_support')); 13585 } 13586 }, 13587 13588 // Override unlink command 13589 unlink : function(command) { 13590 if (selection.isCollapsed()) 13591 selection.select(selection.getNode()); 13592 13593 execNativeCommand(command); 13594 selection.collapse(FALSE); 13595 }, 13596 13597 // Override justify commands to use the text formatter engine 13598 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) { 13599 var align = command.substring(7); 13600 13601 // Remove all other alignments first 13602 each('left,center,right,full'.split(','), function(name) { 13603 if (align != name) 13604 formatter.remove('align' + name); 13605 }); 13606 13607 toggleFormat('align' + align); 13608 execCommand('mceRepaint'); 13609 }, 13610 13611 // Override list commands to fix WebKit bug 13612 'InsertUnorderedList,InsertOrderedList' : function(command) { 13613 var listElm, listParent; 13614 13615 execNativeCommand(command); 13616 13617 // WebKit produces lists within block elements so we need to split them 13618 // we will replace the native list creation logic to custom logic later on 13619 // TODO: Remove this when the list creation logic is removed 13620 listElm = dom.getParent(selection.getNode(), 'ol,ul'); 13621 if (listElm) { 13622 listParent = listElm.parentNode; 13623 13624 // If list is within a text block then split that block 13625 if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { 13626 storeSelection(); 13627 dom.split(listParent, listElm); 13628 restoreSelection(); 13629 } 13630 } 13631 }, 13632 13633 // Override commands to use the text formatter engine 13634 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { 13635 toggleFormat(command); 13636 }, 13637 13638 // Override commands to use the text formatter engine 13639 'ForeColor,HiliteColor,FontName' : function(command, ui, value) { 13640 toggleFormat(command, value); 13641 }, 13642 13643 FontSize : function(command, ui, value) { 13644 var fontClasses, fontSizes; 13645 13646 // Convert font size 1-7 to styles 13647 if (value >= 1 && value <= 7) { 13648 fontSizes = tinymce.explode(settings.font_size_style_values); 13649 fontClasses = tinymce.explode(settings.font_size_classes); 13650 13651 if (fontClasses) 13652 value = fontClasses[value - 1] || value; 13653 else 13654 value = fontSizes[value - 1] || value; 13655 } 13656 13657 toggleFormat(command, value); 13658 }, 13659 13660 RemoveFormat : function(command) { 13661 formatter.remove(command); 13662 }, 13663 13664 mceBlockQuote : function(command) { 13665 toggleFormat('blockquote'); 13666 }, 13667 13668 FormatBlock : function(command, ui, value) { 13669 return toggleFormat(value || 'p'); 13670 }, 13671 13672 mceCleanup : function() { 13673 var bookmark = selection.getBookmark(); 13674 13675 editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE}); 13676 13677 selection.moveToBookmark(bookmark); 13678 }, 13679 13680 mceRemoveNode : function(command, ui, value) { 13681 var node = value || selection.getNode(); 13682 13683 // Make sure that the body node isn't removed 13684 if (node != editor.getBody()) { 13685 storeSelection(); 13686 editor.dom.remove(node, TRUE); 13687 restoreSelection(); 13688 } 13689 }, 13690 13691 mceSelectNodeDepth : function(command, ui, value) { 13692 var counter = 0; 13693 13694 dom.getParent(selection.getNode(), function(node) { 13695 if (node.nodeType == 1 && counter++ == value) { 13696 selection.select(node); 13697 return FALSE; 13698 } 13699 }, editor.getBody()); 13700 }, 13701 13702 mceSelectNode : function(command, ui, value) { 13703 selection.select(value); 13704 }, 13705 13706 mceInsertContent : function(command, ui, value) { 13707 var parser, serializer, parentNode, rootNode, fragment, args, 13708 marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement; 13709 13710 //selection.normalize(); 13711 13712 // Setup parser and serializer 13713 parser = editor.parser; 13714 serializer = new tinymce.html.Serializer({}, editor.schema); 13715 bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">\uFEFF</span>'; 13716 13717 // Run beforeSetContent handlers on the HTML to be inserted 13718 args = {content: value, format: 'html'}; 13719 selection.onBeforeSetContent.dispatch(selection, args); 13720 value = args.content; 13721 13722 // Add caret at end of contents if it's missing 13723 if (value.indexOf('{$caret}') == -1) 13724 value += '{$caret}'; 13725 13726 // Replace the caret marker with a span bookmark element 13727 value = value.replace(/\{\$caret\}/, bookmarkHtml); 13728 13729 // Insert node maker where we will insert the new HTML and get it's parent 13730 if (!selection.isCollapsed()) 13731 editor.getDoc().execCommand('Delete', false, null); 13732 13733 parentNode = selection.getNode(); 13734 13735 // Parse the fragment within the context of the parent node 13736 args = {context : parentNode.nodeName.toLowerCase()}; 13737 fragment = parser.parse(value, args); 13738 13739 // Move the caret to a more suitable location 13740 node = fragment.lastChild; 13741 if (node.attr('id') == 'mce_marker') { 13742 marker = node; 13743 13744 for (node = node.prev; node; node = node.walk(true)) { 13745 if (node.type == 3 || !dom.isBlock(node.name)) { 13746 node.parent.insert(marker, node, node.name === 'br'); 13747 break; 13748 } 13749 } 13750 } 13751 13752 // If parser says valid we can insert the contents into that parent 13753 if (!args.invalid) { 13754 value = serializer.serialize(fragment); 13755 13756 // Check if parent is empty or only has one BR element then set the innerHTML of that parent 13757 node = parentNode.firstChild; 13758 node2 = parentNode.lastChild; 13759 if (!node || (node === node2 && node.nodeName === 'BR')) 13760 dom.setHTML(parentNode, value); 13761 else 13762 selection.setContent(value); 13763 } else { 13764 // If the fragment was invalid within that context then we need 13765 // to parse and process the parent it's inserted into 13766 13767 // Insert bookmark node and get the parent 13768 selection.setContent(bookmarkHtml); 13769 parentNode = editor.selection.getNode(); 13770 rootNode = editor.getBody(); 13771 13772 // Opera will return the document node when selection is in root 13773 if (parentNode.nodeType == 9) 13774 parentNode = node = rootNode; 13775 else 13776 node = parentNode; 13777 13778 // Find the ancestor just before the root element 13779 while (node !== rootNode) { 13780 parentNode = node; 13781 node = node.parentNode; 13782 } 13783 13784 // Get the outer/inner HTML depending on if we are in the root and parser and serialize that 13785 value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); 13786 value = serializer.serialize( 13787 parser.parse( 13788 // Need to replace by using a function since $ in the contents would otherwise be a problem 13789 value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() { 13790 return serializer.serialize(fragment); 13791 }) 13792 ) 13793 ); 13794 13795 // Set the inner/outer HTML depending on if we are in the root or not 13796 if (parentNode == rootNode) 13797 dom.setHTML(rootNode, value); 13798 else 13799 dom.setOuterHTML(parentNode, value); 13800 } 13801 13802 marker = dom.get('mce_marker'); 13803 13804 // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well 13805 nodeRect = dom.getRect(marker); 13806 viewPortRect = dom.getViewPort(editor.getWin()); 13807 13808 // Check if node is out side the viewport if it is then scroll to it 13809 if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) || 13810 (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) { 13811 viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody(); 13812 viewportBodyElement.scrollLeft = nodeRect.x; 13813 viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25; 13814 } 13815 13816 // Move selection before marker and remove it 13817 rng = dom.createRng(); 13818 13819 // If previous sibling is a text node set the selection to the end of that node 13820 node = marker.previousSibling; 13821 if (node && node.nodeType == 3) { 13822 rng.setStart(node, node.nodeValue.length); 13823 } else { 13824 // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node 13825 rng.setStartBefore(marker); 13826 rng.setEndBefore(marker); 13827 } 13828 13829 // Remove the marker node and set the new range 13830 dom.remove(marker); 13831 selection.setRng(rng); 13832 13833 // Dispatch after event and add any visual elements needed 13834 selection.onSetContent.dispatch(selection, args); 13835 editor.addVisual(); 13836 }, 13837 13838 mceInsertRawHTML : function(command, ui, value) { 13839 selection.setContent('tiny_mce_marker'); 13840 editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value })); 13841 }, 13842 13843 mceToggleFormat : function(command, ui, value) { 13844 toggleFormat(value); 13845 }, 13846 13847 mceSetContent : function(command, ui, value) { 13848 editor.setContent(value); 13849 }, 13850 13851 'Indent,Outdent' : function(command) { 13852 var intentValue, indentUnit, value; 13853 13854 // Setup indent level 13855 intentValue = settings.indentation; 13856 indentUnit = /[a-z%]+$/i.exec(intentValue); 13857 intentValue = parseInt(intentValue); 13858 13859 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { 13860 // If forced_root_blocks is set to false we don't have a block to indent so lets create a div 13861 if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { 13862 formatter.apply('div'); 13863 } 13864 13865 each(selection.getSelectedBlocks(), function(element) { 13866 if (command == 'outdent') { 13867 value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue); 13868 dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : ''); 13869 } else 13870 dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit); 13871 }); 13872 } else 13873 execNativeCommand(command); 13874 }, 13875 13876 mceRepaint : function() { 13877 var bookmark; 13878 13879 if (tinymce.isGecko) { 13880 try { 13881 storeSelection(TRUE); 13882 13883 if (selection.getSel()) 13884 selection.getSel().selectAllChildren(editor.getBody()); 13885 13886 selection.collapse(TRUE); 13887 restoreSelection(); 13888 } catch (ex) { 13889 // Ignore 13890 } 13891 } 13892 }, 13893 13894 mceToggleFormat : function(command, ui, value) { 13895 formatter.toggle(value); 13896 }, 13897 13898 InsertHorizontalRule : function() { 13899 editor.execCommand('mceInsertContent', false, '<hr />'); 13900 }, 13901 13902 mceToggleVisualAid : function() { 13903 editor.hasVisual = !editor.hasVisual; 13904 editor.addVisual(); 13905 }, 13906 13907 mceReplaceContent : function(command, ui, value) { 13908 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'}))); 13909 }, 13910 13911 mceInsertLink : function(command, ui, value) { 13912 var anchor; 13913 13914 if (typeof(value) == 'string') 13915 value = {href : value}; 13916 13917 anchor = dom.getParent(selection.getNode(), 'a'); 13918 13919 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. 13920 value.href = value.href.replace(' ', '%20'); 13921 13922 // Remove existing links if there could be child links or that the href isn't specified 13923 if (!anchor || !value.href) { 13924 formatter.remove('link'); 13925 } 13926 13927 // Apply new link to selection 13928 if (value.href) { 13929 formatter.apply('link', value, anchor); 13930 } 13931 }, 13932 13933 selectAll : function() { 13934 var root = dom.getRoot(), rng = dom.createRng(); 13935 13936 rng.setStart(root, 0); 13937 rng.setEnd(root, root.childNodes.length); 13938 13939 editor.selection.setRng(rng); 13940 } 13941 }); 13942 13943 // Add queryCommandState overrides 13944 addCommands({ 13945 // Override justify commands 13946 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) { 13947 var name = 'align' + command.substring(7); 13948 var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks(); 13949 var matches = tinymce.map(nodes, function(node) { 13950 return !!formatter.matchNode(node, name); 13951 }); 13952 return tinymce.inArray(matches, TRUE) !== -1; 13953 }, 13954 13955 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { 13956 return isFormatMatch(command); 13957 }, 13958 13959 mceBlockQuote : function() { 13960 return isFormatMatch('blockquote'); 13961 }, 13962 13963 Outdent : function() { 13964 var node; 13965 13966 if (settings.inline_styles) { 13967 if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0) 13968 return TRUE; 13969 13970 if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0) 13971 return TRUE; 13972 } 13973 13974 return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')); 13975 }, 13976 13977 'InsertUnorderedList,InsertOrderedList' : function(command) { 13978 return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL'); 13979 } 13980 }, 'state'); 13981 13982 // Add queryCommandValue overrides 13983 addCommands({ 13984 'FontSize,FontName' : function(command) { 13985 var value = 0, parent; 13986 13987 if (parent = dom.getParent(selection.getNode(), 'span')) { 13988 if (command == 'fontsize') 13989 value = parent.style.fontSize; 13990 else 13991 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); 13992 } 13993 13994 return value; 13995 } 13996 }, 'value'); 13997 13998 // Add undo manager logic 13999 addCommands({ 14000 Undo : function() { 14001 editor.undoManager.undo(); 14002 }, 14003 14004 Redo : function() { 14005 editor.undoManager.redo(); 14006 } 14007 }); 14008 }; 14009 })(tinymce); 14010 14011 (function(tinymce) { 14012 var Dispatcher = tinymce.util.Dispatcher; 14013 14014 tinymce.UndoManager = function(editor) { 14015 var self, index = 0, data = [], beforeBookmark, onAdd, onUndo, onRedo; 14016 14017 function getContent() { 14018 // Remove whitespace before/after and remove pure bogus nodes 14019 return tinymce.trim(editor.getContent({format : 'raw', no_events : 1}).replace(/<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\/span>/g, '')); 14020 }; 14021 14022 function addNonTypingUndoLevel() { 14023 self.typing = false; 14024 self.add(); 14025 }; 14026 14027 // Create event instances 14028 onBeforeAdd = new Dispatcher(self); 14029 onAdd = new Dispatcher(self); 14030 onUndo = new Dispatcher(self); 14031 onRedo = new Dispatcher(self); 14032 14033 // Pass though onAdd event from UndoManager to Editor as onChange 14034 onAdd.add(function(undoman, level) { 14035 if (undoman.hasUndo()) 14036 return editor.onChange.dispatch(editor, level, undoman); 14037 }); 14038 14039 // Pass though onUndo event from UndoManager to Editor 14040 onUndo.add(function(undoman, level) { 14041 return editor.onUndo.dispatch(editor, level, undoman); 14042 }); 14043 14044 // Pass though onRedo event from UndoManager to Editor 14045 onRedo.add(function(undoman, level) { 14046 return editor.onRedo.dispatch(editor, level, undoman); 14047 }); 14048 14049 // Add initial undo level when the editor is initialized 14050 editor.onInit.add(function() { 14051 self.add(); 14052 }); 14053 14054 // Get position before an execCommand is processed 14055 editor.onBeforeExecCommand.add(function(ed, cmd, ui, val, args) { 14056 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) { 14057 self.beforeChange(); 14058 } 14059 }); 14060 14061 // Add undo level after an execCommand call was made 14062 editor.onExecCommand.add(function(ed, cmd, ui, val, args) { 14063 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) { 14064 self.add(); 14065 } 14066 }); 14067 14068 // Add undo level on save contents, drag end and blur/focusout 14069 editor.onSaveContent.add(addNonTypingUndoLevel); 14070 editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel); 14071 editor.dom.bind(editor.getDoc(), tinymce.isGecko ? 'blur' : 'focusout', function(e) { 14072 if (!editor.removed && self.typing) { 14073 addNonTypingUndoLevel(); 14074 } 14075 }); 14076 14077 editor.onKeyUp.add(function(editor, e) { 14078 var keyCode = e.keyCode; 14079 14080 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) { 14081 addNonTypingUndoLevel(); 14082 } 14083 }); 14084 14085 editor.onKeyDown.add(function(editor, e) { 14086 var keyCode = e.keyCode; 14087 14088 // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter 14089 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) { 14090 if (self.typing) { 14091 addNonTypingUndoLevel(); 14092 } 14093 14094 return; 14095 } 14096 14097 // If key isn't shift,ctrl,alt,capslock,metakey 14098 if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) { 14099 self.beforeChange(); 14100 self.typing = true; 14101 self.add(); 14102 } 14103 }); 14104 14105 editor.onMouseDown.add(function(editor, e) { 14106 if (self.typing) { 14107 addNonTypingUndoLevel(); 14108 } 14109 }); 14110 14111 // Add keyboard shortcuts for undo/redo keys 14112 editor.addShortcut('ctrl+z', 'undo_desc', 'Undo'); 14113 editor.addShortcut('ctrl+y', 'redo_desc', 'Redo'); 14114 14115 self = { 14116 // Explose for debugging reasons 14117 data : data, 14118 14119 typing : false, 14120 14121 onBeforeAdd: onBeforeAdd, 14122 14123 onAdd : onAdd, 14124 14125 onUndo : onUndo, 14126 14127 onRedo : onRedo, 14128 14129 beforeChange : function() { 14130 beforeBookmark = editor.selection.getBookmark(2, true); 14131 }, 14132 14133 add : function(level) { 14134 var i, settings = editor.settings, lastLevel; 14135 14136 level = level || {}; 14137 level.content = getContent(); 14138 14139 self.onBeforeAdd.dispatch(self, level); 14140 14141 // Add undo level if needed 14142 lastLevel = data[index]; 14143 if (lastLevel && lastLevel.content == level.content) 14144 return null; 14145 14146 // Set before bookmark on previous level 14147 if (data[index]) 14148 data[index].beforeBookmark = beforeBookmark; 14149 14150 // Time to compress 14151 if (settings.custom_undo_redo_levels) { 14152 if (data.length > settings.custom_undo_redo_levels) { 14153 for (i = 0; i < data.length - 1; i++) 14154 data[i] = data[i + 1]; 14155 14156 data.length--; 14157 index = data.length; 14158 } 14159 } 14160 14161 // Get a non intrusive normalized bookmark 14162 level.bookmark = editor.selection.getBookmark(2, true); 14163 14164 // Crop array if needed 14165 if (index < data.length - 1) 14166 data.length = index + 1; 14167 14168 data.push(level); 14169 index = data.length - 1; 14170 14171 self.onAdd.dispatch(self, level); 14172 editor.isNotDirty = 0; 14173 14174 return level; 14175 }, 14176 14177 undo : function() { 14178 var level, i; 14179 14180 if (self.typing) { 14181 self.add(); 14182 self.typing = false; 14183 } 14184 14185 if (index > 0) { 14186 level = data[--index]; 14187 14188 editor.setContent(level.content, {format : 'raw'}); 14189 editor.selection.moveToBookmark(level.beforeBookmark); 14190 14191 self.onUndo.dispatch(self, level); 14192 } 14193 14194 return level; 14195 }, 14196 14197 redo : function() { 14198 var level; 14199 14200 if (index < data.length - 1) { 14201 level = data[++index]; 14202 14203 editor.setContent(level.content, {format : 'raw'}); 14204 editor.selection.moveToBookmark(level.bookmark); 14205 14206 self.onRedo.dispatch(self, level); 14207 } 14208 14209 return level; 14210 }, 14211 14212 clear : function() { 14213 data = []; 14214 index = 0; 14215 self.typing = false; 14216 }, 14217 14218 hasUndo : function() { 14219 return index > 0 || this.typing; 14220 }, 14221 14222 hasRedo : function() { 14223 return index < data.length - 1 && !this.typing; 14224 } 14225 }; 14226 14227 return self; 14228 }; 14229 })(tinymce); 14230 14231 tinymce.ForceBlocks = function(editor) { 14232 var settings = editor.settings, dom = editor.dom, selection = editor.selection, blockElements = editor.schema.getBlockElements(); 14233 14234 function addRootBlocks() { 14235 var node = selection.getStart(), rootNode = editor.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF, wrapped, isInEditorDocument; 14236 14237 if (!node || node.nodeType !== 1 || !settings.forced_root_block) 14238 return; 14239 14240 // Check if node is wrapped in block 14241 while (node && node != rootNode) { 14242 if (blockElements[node.nodeName]) 14243 return; 14244 14245 node = node.parentNode; 14246 } 14247 14248 // Get current selection 14249 rng = selection.getRng(); 14250 if (rng.setStart) { 14251 startContainer = rng.startContainer; 14252 startOffset = rng.startOffset; 14253 endContainer = rng.endContainer; 14254 endOffset = rng.endOffset; 14255 } else { 14256 // Force control range into text range 14257 if (rng.item) { 14258 node = rng.item(0); 14259 rng = editor.getDoc().body.createTextRange(); 14260 rng.moveToElementText(node); 14261 } 14262 14263 isInEditorDocument = rng.parentElement().ownerDocument === editor.getDoc(); 14264 tmpRng = rng.duplicate(); 14265 tmpRng.collapse(true); 14266 startOffset = tmpRng.move('character', offset) * -1; 14267 14268 if (!tmpRng.collapsed) { 14269 tmpRng = rng.duplicate(); 14270 tmpRng.collapse(false); 14271 endOffset = (tmpRng.move('character', offset) * -1) - startOffset; 14272 } 14273 } 14274 14275 // Wrap non block elements and text nodes 14276 node = rootNode.firstChild; 14277 while (node) { 14278 if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) { 14279 if (!rootBlockNode) { 14280 rootBlockNode = dom.create(settings.forced_root_block); 14281 node.parentNode.insertBefore(rootBlockNode, node); 14282 wrapped = true; 14283 } 14284 14285 tempNode = node; 14286 node = node.nextSibling; 14287 rootBlockNode.appendChild(tempNode); 14288 } else { 14289 rootBlockNode = null; 14290 node = node.nextSibling; 14291 } 14292 } 14293 14294 if (wrapped) { 14295 if (rng.setStart) { 14296 rng.setStart(startContainer, startOffset); 14297 rng.setEnd(endContainer, endOffset); 14298 selection.setRng(rng); 14299 } else { 14300 // Only select if the previous selection was inside the document to prevent auto focus in quirks mode 14301 if (isInEditorDocument) { 14302 try { 14303 rng = editor.getDoc().body.createTextRange(); 14304 rng.moveToElementText(rootNode); 14305 rng.collapse(true); 14306 rng.moveStart('character', startOffset); 14307 14308 if (endOffset > 0) 14309 rng.moveEnd('character', endOffset); 14310 14311 rng.select(); 14312 } catch (ex) { 14313 // Ignore 14314 } 14315 } 14316 } 14317 14318 editor.nodeChanged(); 14319 } 14320 }; 14321 14322 // Force root blocks 14323 if (settings.forced_root_block) { 14324 editor.onKeyUp.add(addRootBlocks); 14325 editor.onNodeChange.add(addRootBlocks); 14326 } 14327 }; 14328 14329 (function(tinymce) { 14330 // Shorten names 14331 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend; 14332 14333 tinymce.create('tinymce.ControlManager', { 14334 ControlManager : function(ed, s) { 14335 var t = this, i; 14336 14337 s = s || {}; 14338 t.editor = ed; 14339 t.controls = {}; 14340 t.onAdd = new tinymce.util.Dispatcher(t); 14341 t.onPostRender = new tinymce.util.Dispatcher(t); 14342 t.prefix = s.prefix || ed.id + '_'; 14343 t._cls = {}; 14344 14345 t.onPostRender.add(function() { 14346 each(t.controls, function(c) { 14347 c.postRender(); 14348 }); 14349 }); 14350 }, 14351 14352 get : function(id) { 14353 return this.controls[this.prefix + id] || this.controls[id]; 14354 }, 14355 14356 setActive : function(id, s) { 14357 var c = null; 14358 14359 if (c = this.get(id)) 14360 c.setActive(s); 14361 14362 return c; 14363 }, 14364 14365 setDisabled : function(id, s) { 14366 var c = null; 14367 14368 if (c = this.get(id)) 14369 c.setDisabled(s); 14370 14371 return c; 14372 }, 14373 14374 add : function(c) { 14375 var t = this; 14376 14377 if (c) { 14378 t.controls[c.id] = c; 14379 t.onAdd.dispatch(c, t); 14380 } 14381 14382 return c; 14383 }, 14384 14385 createControl : function(name) { 14386 var ctrl, i, l, self = this, editor = self.editor, factories, ctrlName; 14387 14388 // Build control factory cache 14389 if (!self.controlFactories) { 14390 self.controlFactories = []; 14391 each(editor.plugins, function(plugin) { 14392 if (plugin.createControl) { 14393 self.controlFactories.push(plugin); 14394 } 14395 }); 14396 } 14397 14398 // Create controls by asking cached factories 14399 factories = self.controlFactories; 14400 for (i = 0, l = factories.length; i < l; i++) { 14401 ctrl = factories[i].createControl(name, self); 14402 14403 if (ctrl) { 14404 return self.add(ctrl); 14405 } 14406 } 14407 14408 // Create sepearator 14409 if (name === "|" || name === "separator") { 14410 return self.createSeparator(); 14411 } 14412 14413 // Create control from button collection 14414 if (editor.buttons && (ctrl = editor.buttons[name])) { 14415 return self.createButton(name, ctrl); 14416 } 14417 14418 return self.add(ctrl); 14419 }, 14420 14421 createDropMenu : function(id, s, cc) { 14422 var t = this, ed = t.editor, c, bm, v, cls; 14423 14424 s = extend({ 14425 'class' : 'mceDropDown', 14426 constrain : ed.settings.constrain_menus 14427 }, s); 14428 14429 s['class'] = s['class'] + ' ' + ed.getParam('skin') + 'Skin'; 14430 if (v = ed.getParam('skin_variant')) 14431 s['class'] += ' ' + ed.getParam('skin') + 'Skin' + v.substring(0, 1).toUpperCase() + v.substring(1); 14432 14433 s['class'] += ed.settings.directionality == "rtl" ? ' mceRtl' : ''; 14434 14435 id = t.prefix + id; 14436 cls = cc || t._cls.dropmenu || tinymce.ui.DropMenu; 14437 c = t.controls[id] = new cls(id, s); 14438 c.onAddItem.add(function(c, o) { 14439 var s = o.settings; 14440 14441 s.title = ed.getLang(s.title, s.title); 14442 14443 if (!s.onclick) { 14444 s.onclick = function(v) { 14445 if (s.cmd) 14446 ed.execCommand(s.cmd, s.ui || false, s.value); 14447 }; 14448 } 14449 }); 14450 14451 ed.onRemove.add(function() { 14452 c.destroy(); 14453 }); 14454 14455 // Fix for bug #1897785, #1898007 14456 if (tinymce.isIE) { 14457 c.onShowMenu.add(function() { 14458 // IE 8 needs focus in order to store away a range with the current collapsed caret location 14459 ed.focus(); 14460 14461 bm = ed.selection.getBookmark(1); 14462 }); 14463 14464 c.onHideMenu.add(function() { 14465 if (bm) { 14466 ed.selection.moveToBookmark(bm); 14467 bm = 0; 14468 } 14469 }); 14470 } 14471 14472 return t.add(c); 14473 }, 14474 14475 createListBox : function(id, s, cc) { 14476 var t = this, ed = t.editor, cmd, c, cls; 14477 14478 if (t.get(id)) 14479 return null; 14480 14481 s.title = ed.translate(s.title); 14482 s.scope = s.scope || ed; 14483 14484 if (!s.onselect) { 14485 s.onselect = function(v) { 14486 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14487 }; 14488 } 14489 14490 s = extend({ 14491 title : s.title, 14492 'class' : 'mce_' + id, 14493 scope : s.scope, 14494 control_manager : t 14495 }, s); 14496 14497 id = t.prefix + id; 14498 14499 14500 function useNativeListForAccessibility(ed) { 14501 return ed.settings.use_accessible_selects && !tinymce.isGecko 14502 } 14503 14504 if (ed.settings.use_native_selects || useNativeListForAccessibility(ed)) 14505 c = new tinymce.ui.NativeListBox(id, s); 14506 else { 14507 cls = cc || t._cls.listbox || tinymce.ui.ListBox; 14508 c = new cls(id, s, ed); 14509 } 14510 14511 t.controls[id] = c; 14512 14513 // Fix focus problem in Safari 14514 if (tinymce.isWebKit) { 14515 c.onPostRender.add(function(c, n) { 14516 // Store bookmark on mousedown 14517 Event.add(n, 'mousedown', function() { 14518 ed.bookmark = ed.selection.getBookmark(1); 14519 }); 14520 14521 // Restore on focus, since it might be lost 14522 Event.add(n, 'focus', function() { 14523 ed.selection.moveToBookmark(ed.bookmark); 14524 ed.bookmark = null; 14525 }); 14526 }); 14527 } 14528 14529 if (c.hideMenu) 14530 ed.onMouseDown.add(c.hideMenu, c); 14531 14532 return t.add(c); 14533 }, 14534 14535 createButton : function(id, s, cc) { 14536 var t = this, ed = t.editor, o, c, cls; 14537 14538 if (t.get(id)) 14539 return null; 14540 14541 s.title = ed.translate(s.title); 14542 s.label = ed.translate(s.label); 14543 s.scope = s.scope || ed; 14544 14545 if (!s.onclick && !s.menu_button) { 14546 s.onclick = function() { 14547 ed.execCommand(s.cmd, s.ui || false, s.value); 14548 }; 14549 } 14550 14551 s = extend({ 14552 title : s.title, 14553 'class' : 'mce_' + id, 14554 unavailable_prefix : ed.getLang('unavailable', ''), 14555 scope : s.scope, 14556 control_manager : t 14557 }, s); 14558 14559 id = t.prefix + id; 14560 14561 if (s.menu_button) { 14562 cls = cc || t._cls.menubutton || tinymce.ui.MenuButton; 14563 c = new cls(id, s, ed); 14564 ed.onMouseDown.add(c.hideMenu, c); 14565 } else { 14566 cls = t._cls.button || tinymce.ui.Button; 14567 c = new cls(id, s, ed); 14568 } 14569 14570 return t.add(c); 14571 }, 14572 14573 createMenuButton : function(id, s, cc) { 14574 s = s || {}; 14575 s.menu_button = 1; 14576 14577 return this.createButton(id, s, cc); 14578 }, 14579 14580 createSplitButton : function(id, s, cc) { 14581 var t = this, ed = t.editor, cmd, c, cls; 14582 14583 if (t.get(id)) 14584 return null; 14585 14586 s.title = ed.translate(s.title); 14587 s.scope = s.scope || ed; 14588 14589 if (!s.onclick) { 14590 s.onclick = function(v) { 14591 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14592 }; 14593 } 14594 14595 if (!s.onselect) { 14596 s.onselect = function(v) { 14597 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14598 }; 14599 } 14600 14601 s = extend({ 14602 title : s.title, 14603 'class' : 'mce_' + id, 14604 scope : s.scope, 14605 control_manager : t 14606 }, s); 14607 14608 id = t.prefix + id; 14609 cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton; 14610 c = t.add(new cls(id, s, ed)); 14611 ed.onMouseDown.add(c.hideMenu, c); 14612 14613 return c; 14614 }, 14615 14616 createColorSplitButton : function(id, s, cc) { 14617 var t = this, ed = t.editor, cmd, c, cls, bm; 14618 14619 if (t.get(id)) 14620 return null; 14621 14622 s.title = ed.translate(s.title); 14623 s.scope = s.scope || ed; 14624 14625 if (!s.onclick) { 14626 s.onclick = function(v) { 14627 if (tinymce.isIE) 14628 bm = ed.selection.getBookmark(1); 14629 14630 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14631 }; 14632 } 14633 14634 if (!s.onselect) { 14635 s.onselect = function(v) { 14636 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14637 }; 14638 } 14639 14640 s = extend({ 14641 title : s.title, 14642 'class' : 'mce_' + id, 14643 'menu_class' : ed.getParam('skin') + 'Skin', 14644 scope : s.scope, 14645 more_colors_title : ed.getLang('more_colors') 14646 }, s); 14647 14648 id = t.prefix + id; 14649 cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton; 14650 c = new cls(id, s, ed); 14651 ed.onMouseDown.add(c.hideMenu, c); 14652 14653 // Remove the menu element when the editor is removed 14654 ed.onRemove.add(function() { 14655 c.destroy(); 14656 }); 14657 14658 // Fix for bug #1897785, #1898007 14659 if (tinymce.isIE) { 14660 c.onShowMenu.add(function() { 14661 // IE 8 needs focus in order to store away a range with the current collapsed caret location 14662 ed.focus(); 14663 bm = ed.selection.getBookmark(1); 14664 }); 14665 14666 c.onHideMenu.add(function() { 14667 if (bm) { 14668 ed.selection.moveToBookmark(bm); 14669 bm = 0; 14670 } 14671 }); 14672 } 14673 14674 return t.add(c); 14675 }, 14676 14677 createToolbar : function(id, s, cc) { 14678 var c, t = this, cls; 14679 14680 id = t.prefix + id; 14681 cls = cc || t._cls.toolbar || tinymce.ui.Toolbar; 14682 c = new cls(id, s, t.editor); 14683 14684 if (t.get(id)) 14685 return null; 14686 14687 return t.add(c); 14688 }, 14689 14690 createToolbarGroup : function(id, s, cc) { 14691 var c, t = this, cls; 14692 id = t.prefix + id; 14693 cls = cc || this._cls.toolbarGroup || tinymce.ui.ToolbarGroup; 14694 c = new cls(id, s, t.editor); 14695 14696 if (t.get(id)) 14697 return null; 14698 14699 return t.add(c); 14700 }, 14701 14702 createSeparator : function(cc) { 14703 var cls = cc || this._cls.separator || tinymce.ui.Separator; 14704 14705 return new cls(); 14706 }, 14707 14708 setControlType : function(n, c) { 14709 return this._cls[n.toLowerCase()] = c; 14710 }, 14711 14712 destroy : function() { 14713 each(this.controls, function(c) { 14714 c.destroy(); 14715 }); 14716 14717 this.controls = null; 14718 } 14719 }); 14720 })(tinymce); 14721 14722 (function(tinymce) { 14723 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera; 14724 14725 tinymce.create('tinymce.WindowManager', { 14726 WindowManager : function(ed) { 14727 var t = this; 14728 14729 t.editor = ed; 14730 t.onOpen = new Dispatcher(t); 14731 t.onClose = new Dispatcher(t); 14732 t.params = {}; 14733 t.features = {}; 14734 }, 14735 14736 open : function(s, p) { 14737 var t = this, f = '', x, y, mo = t.editor.settings.dialog_type == 'modal', w, sw, sh, vp = tinymce.DOM.getViewPort(), u; 14738 14739 // Default some options 14740 s = s || {}; 14741 p = p || {}; 14742 sw = isOpera ? vp.w : screen.width; // Opera uses windows inside the Opera window 14743 sh = isOpera ? vp.h : screen.height; 14744 s.name = s.name || 'mc_' + new Date().getTime(); 14745 s.width = parseInt(s.width || 320); 14746 s.height = parseInt(s.height || 240); 14747 s.resizable = true; 14748 s.left = s.left || parseInt(sw / 2.0) - (s.width / 2.0); 14749 s.top = s.top || parseInt(sh / 2.0) - (s.height / 2.0); 14750 p.inline = false; 14751 p.mce_width = s.width; 14752 p.mce_height = s.height; 14753 p.mce_auto_focus = s.auto_focus; 14754 14755 if (mo) { 14756 if (isIE) { 14757 s.center = true; 14758 s.help = false; 14759 s.dialogWidth = s.width + 'px'; 14760 s.dialogHeight = s.height + 'px'; 14761 s.scroll = s.scrollbars || false; 14762 } 14763 } 14764 14765 // Build features string 14766 each(s, function(v, k) { 14767 if (tinymce.is(v, 'boolean')) 14768 v = v ? 'yes' : 'no'; 14769 14770 if (!/^(name|url)$/.test(k)) { 14771 if (isIE && mo) 14772 f += (f ? ';' : '') + k + ':' + v; 14773 else 14774 f += (f ? ',' : '') + k + '=' + v; 14775 } 14776 }); 14777 14778 t.features = s; 14779 t.params = p; 14780 t.onOpen.dispatch(t, s, p); 14781 14782 u = s.url || s.file; 14783 u = tinymce._addVer(u); 14784 14785 try { 14786 if (isIE && mo) { 14787 w = 1; 14788 window.showModalDialog(u, window, f); 14789 } else 14790 w = window.open(u, s.name, f); 14791 } catch (ex) { 14792 // Ignore 14793 } 14794 14795 if (!w) 14796 alert(t.editor.getLang('popup_blocked')); 14797 }, 14798 14799 close : function(w) { 14800 w.close(); 14801 this.onClose.dispatch(this); 14802 }, 14803 14804 createInstance : function(cl, a, b, c, d, e) { 14805 var f = tinymce.resolve(cl); 14806 14807 return new f(a, b, c, d, e); 14808 }, 14809 14810 confirm : function(t, cb, s, w) { 14811 w = w || window; 14812 14813 cb.call(s || this, w.confirm(this._decode(this.editor.getLang(t, t)))); 14814 }, 14815 14816 alert : function(tx, cb, s, w) { 14817 var t = this; 14818 14819 w = w || window; 14820 w.alert(t._decode(t.editor.getLang(tx, tx))); 14821 14822 if (cb) 14823 cb.call(s || t); 14824 }, 14825 14826 resizeBy : function(dw, dh, win) { 14827 win.resizeBy(dw, dh); 14828 }, 14829 14830 // Internal functions 14831 14832 _decode : function(s) { 14833 return tinymce.DOM.decode(s).replace(/\\n/g, '\n'); 14834 } 14835 }); 14836 }(tinymce)); 14837 (function(tinymce) { 14838 tinymce.Formatter = function(ed) { 14839 var formats = {}, 14840 each = tinymce.each, 14841 dom = ed.dom, 14842 selection = ed.selection, 14843 TreeWalker = tinymce.dom.TreeWalker, 14844 rangeUtils = new tinymce.dom.RangeUtils(dom), 14845 isValid = ed.schema.isValidChild, 14846 isBlock = dom.isBlock, 14847 forcedRootBlock = ed.settings.forced_root_block, 14848 nodeIndex = dom.nodeIndex, 14849 INVISIBLE_CHAR = tinymce.isGecko ? '\u200B' : '\uFEFF', 14850 MCE_ATTR_RE = /^(src|href|style)$/, 14851 FALSE = false, 14852 TRUE = true, 14853 formatChangeData, 14854 undef, 14855 getContentEditable = dom.getContentEditable; 14856 14857 function isArray(obj) { 14858 return obj instanceof Array; 14859 }; 14860 14861 function getParents(node, selector) { 14862 return dom.getParents(node, selector, dom.getRoot()); 14863 }; 14864 14865 function isCaretNode(node) { 14866 return node.nodeType === 1 && node.id === '_mce_caret'; 14867 }; 14868 14869 function defaultFormats() { 14870 register({ 14871 alignleft : [ 14872 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'left'}, defaultBlock: 'div'}, 14873 {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}} 14874 ], 14875 14876 aligncenter : [ 14877 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}, defaultBlock: 'div'}, 14878 {selector : 'img', collapsed : false, styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}}, 14879 {selector : 'table', collapsed : false, styles : {marginLeft : 'auto', marginRight : 'auto'}} 14880 ], 14881 14882 alignright : [ 14883 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}, defaultBlock: 'div'}, 14884 {selector : 'img,table', collapsed : false, styles : {'float' : 'right'}} 14885 ], 14886 14887 alignfull : [ 14888 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'justify'}, defaultBlock: 'div'} 14889 ], 14890 14891 bold : [ 14892 {inline : 'strong', remove : 'all'}, 14893 {inline : 'span', styles : {fontWeight : 'bold'}}, 14894 {inline : 'b', remove : 'all'} 14895 ], 14896 14897 italic : [ 14898 {inline : 'em', remove : 'all'}, 14899 {inline : 'span', styles : {fontStyle : 'italic'}}, 14900 {inline : 'i', remove : 'all'} 14901 ], 14902 14903 underline : [ 14904 {inline : 'span', styles : {textDecoration : 'underline'}, exact : true}, 14905 {inline : 'u', remove : 'all'} 14906 ], 14907 14908 strikethrough : [ 14909 {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true}, 14910 {inline : 'strike', remove : 'all'} 14911 ], 14912 14913 forecolor : {inline : 'span', styles : {color : '%value'}, wrap_links : false}, 14914 hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}, wrap_links : false}, 14915 fontname : {inline : 'span', styles : {fontFamily : '%value'}}, 14916 fontsize : {inline : 'span', styles : {fontSize : '%value'}}, 14917 fontsize_class : {inline : 'span', attributes : {'class' : '%value'}}, 14918 blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'}, 14919 subscript : {inline : 'sub'}, 14920 superscript : {inline : 'sup'}, 14921 14922 link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true, 14923 onmatch : function(node) { 14924 return true; 14925 }, 14926 14927 onformat : function(elm, fmt, vars) { 14928 each(vars, function(value, key) { 14929 dom.setAttrib(elm, key, value); 14930 }); 14931 } 14932 }, 14933 14934 removeformat : [ 14935 {selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true}, 14936 {selector : 'span', attributes : ['style', 'class'], remove : 'empty', split : true, expand : false, deep : true}, 14937 {selector : '*', attributes : ['style', 'class'], split : false, expand : false, deep : true} 14938 ] 14939 }); 14940 14941 // Register default block formats 14942 each('p h1 h2 h3 h4 h5 h6 div address pre div code dt dd samp'.split(/\s/), function(name) { 14943 register(name, {block : name, remove : 'all'}); 14944 }); 14945 14946 // Register user defined formats 14947 register(ed.settings.formats); 14948 }; 14949 14950 function addKeyboardShortcuts() { 14951 // Add some inline shortcuts 14952 ed.addShortcut('ctrl+b', 'bold_desc', 'Bold'); 14953 ed.addShortcut('ctrl+i', 'italic_desc', 'Italic'); 14954 ed.addShortcut('ctrl+u', 'underline_desc', 'Underline'); 14955 14956 // BlockFormat shortcuts keys 14957 for (var i = 1; i <= 6; i++) { 14958 ed.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); 14959 } 14960 14961 ed.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); 14962 ed.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); 14963 ed.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); 14964 }; 14965 14966 // Public functions 14967 14968 function get(name) { 14969 return name ? formats[name] : formats; 14970 }; 14971 14972 function register(name, format) { 14973 if (name) { 14974 if (typeof(name) !== 'string') { 14975 each(name, function(format, name) { 14976 register(name, format); 14977 }); 14978 } else { 14979 // Force format into array and add it to internal collection 14980 format = format.length ? format : [format]; 14981 14982 each(format, function(format) { 14983 // Set deep to false by default on selector formats this to avoid removing 14984 // alignment on images inside paragraphs when alignment is changed on paragraphs 14985 if (format.deep === undef) 14986 format.deep = !format.selector; 14987 14988 // Default to true 14989 if (format.split === undef) 14990 format.split = !format.selector || format.inline; 14991 14992 // Default to true 14993 if (format.remove === undef && format.selector && !format.inline) 14994 format.remove = 'none'; 14995 14996 // Mark format as a mixed format inline + block level 14997 if (format.selector && format.inline) { 14998 format.mixed = true; 14999 format.block_expand = true; 15000 } 15001 15002 // Split classes if needed 15003 if (typeof(format.classes) === 'string') 15004 format.classes = format.classes.split(/\s+/); 15005 }); 15006 15007 formats[name] = format; 15008 } 15009 } 15010 }; 15011 15012 var getTextDecoration = function(node) { 15013 var decoration; 15014 15015 ed.dom.getParent(node, function(n) { 15016 decoration = ed.dom.getStyle(n, 'text-decoration'); 15017 return decoration && decoration !== 'none'; 15018 }); 15019 15020 return decoration; 15021 }; 15022 15023 var processUnderlineAndColor = function(node) { 15024 var textDecoration; 15025 if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { 15026 textDecoration = getTextDecoration(node.parentNode); 15027 if (ed.dom.getStyle(node, 'color') && textDecoration) { 15028 ed.dom.setStyle(node, 'text-decoration', textDecoration); 15029 } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) { 15030 ed.dom.setStyle(node, 'text-decoration', null); 15031 } 15032 } 15033 }; 15034 15035 function apply(name, vars, node) { 15036 var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed(); 15037 15038 function setElementFormat(elm, fmt) { 15039 fmt = fmt || format; 15040 15041 if (elm) { 15042 if (fmt.onformat) { 15043 fmt.onformat(elm, fmt, vars, node); 15044 } 15045 15046 each(fmt.styles, function(value, name) { 15047 dom.setStyle(elm, name, replaceVars(value, vars)); 15048 }); 15049 15050 each(fmt.attributes, function(value, name) { 15051 dom.setAttrib(elm, name, replaceVars(value, vars)); 15052 }); 15053 15054 each(fmt.classes, function(value) { 15055 value = replaceVars(value, vars); 15056 15057 if (!dom.hasClass(elm, value)) 15058 dom.addClass(elm, value); 15059 }); 15060 } 15061 }; 15062 function adjustSelectionToVisibleSelection() { 15063 function findSelectionEnd(start, end) { 15064 var walker = new TreeWalker(end); 15065 for (node = walker.current(); node; node = walker.prev()) { 15066 if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { 15067 return node; 15068 } 15069 } 15070 }; 15071 15072 // Adjust selection so that a end container with a end offset of zero is not included in the selection 15073 // as this isn't visible to the user. 15074 var rng = ed.selection.getRng(); 15075 var start = rng.startContainer; 15076 var end = rng.endContainer; 15077 15078 if (start != end && rng.endOffset === 0) { 15079 var newEnd = findSelectionEnd(start, end); 15080 var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length; 15081 15082 rng.setEnd(newEnd, endOffset); 15083 } 15084 15085 return rng; 15086 } 15087 15088 function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){ 15089 var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm; 15090 15091 // find the index of the first child list. 15092 each(node.childNodes, function(n, index) { 15093 if (n.nodeName === "UL" || n.nodeName === "OL") { 15094 listIndex = index; 15095 list = n; 15096 return false; 15097 } 15098 }); 15099 15100 // get the index of the bookmarks 15101 each(node.childNodes, function(n, index) { 15102 if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") { 15103 if (n.id == bookmark.id + "_start") { 15104 startIndex = index; 15105 } else if (n.id == bookmark.id + "_end") { 15106 endIndex = index; 15107 } 15108 } 15109 }); 15110 15111 // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally 15112 if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) { 15113 each(tinymce.grep(node.childNodes), process); 15114 return 0; 15115 } else { 15116 currentWrapElm = dom.clone(wrapElm, FALSE); 15117 15118 // create a list of the nodes on the same side of the list as the selection 15119 each(tinymce.grep(node.childNodes), function(n, index) { 15120 if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) { 15121 nodes.push(n); 15122 n.parentNode.removeChild(n); 15123 } 15124 }); 15125 15126 // insert the wrapping element either before or after the list. 15127 if (startIndex < listIndex) { 15128 node.insertBefore(currentWrapElm, list); 15129 } else if (startIndex > listIndex) { 15130 node.insertBefore(currentWrapElm, list.nextSibling); 15131 } 15132 15133 // add the new nodes to the list. 15134 newWrappers.push(currentWrapElm); 15135 15136 each(nodes, function(node) { 15137 currentWrapElm.appendChild(node); 15138 }); 15139 15140 return currentWrapElm; 15141 } 15142 }; 15143 15144 function applyRngStyle(rng, bookmark, node_specific) { 15145 var newWrappers = [], wrapName, wrapElm, contentEditable = true; 15146 15147 // Setup wrapper element 15148 wrapName = format.inline || format.block; 15149 wrapElm = dom.create(wrapName); 15150 setElementFormat(wrapElm); 15151 15152 rangeUtils.walk(rng, function(nodes) { 15153 var currentWrapElm; 15154 15155 function process(node) { 15156 var nodeName, parentName, found, hasContentEditableState, lastContentEditable; 15157 15158 lastContentEditable = contentEditable; 15159 nodeName = node.nodeName.toLowerCase(); 15160 parentName = node.parentNode.nodeName.toLowerCase(); 15161 15162 // Node has a contentEditable value 15163 if (node.nodeType === 1 && getContentEditable(node)) { 15164 lastContentEditable = contentEditable; 15165 contentEditable = getContentEditable(node) === "true"; 15166 hasContentEditableState = true; // We don't want to wrap the container only it's children 15167 } 15168 15169 // Stop wrapping on br elements 15170 if (isEq(nodeName, 'br')) { 15171 currentWrapElm = 0; 15172 15173 // Remove any br elements when we wrap things 15174 if (format.block) 15175 dom.remove(node); 15176 15177 return; 15178 } 15179 15180 // If node is wrapper type 15181 if (format.wrapper && matchNode(node, name, vars)) { 15182 currentWrapElm = 0; 15183 return; 15184 } 15185 15186 // Can we rename the block 15187 if (contentEditable && !hasContentEditableState && format.block && !format.wrapper && isTextBlock(nodeName)) { 15188 node = dom.rename(node, wrapName); 15189 setElementFormat(node); 15190 newWrappers.push(node); 15191 currentWrapElm = 0; 15192 return; 15193 } 15194 15195 // Handle selector patterns 15196 if (format.selector) { 15197 // Look for matching formats 15198 each(formatList, function(format) { 15199 // Check collapsed state if it exists 15200 if ('collapsed' in format && format.collapsed !== isCollapsed) { 15201 return; 15202 } 15203 15204 if (dom.is(node, format.selector) && !isCaretNode(node)) { 15205 setElementFormat(node, format); 15206 found = true; 15207 } 15208 }); 15209 15210 // Continue processing if a selector match wasn't found and a inline element is defined 15211 if (!format.inline || found) { 15212 currentWrapElm = 0; 15213 return; 15214 } 15215 } 15216 15217 // Is it valid to wrap this item 15218 if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && 15219 !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && !isCaretNode(node)) { 15220 // Start wrapping 15221 if (!currentWrapElm) { 15222 // Wrap the node 15223 currentWrapElm = dom.clone(wrapElm, FALSE); 15224 node.parentNode.insertBefore(currentWrapElm, node); 15225 newWrappers.push(currentWrapElm); 15226 } 15227 15228 currentWrapElm.appendChild(node); 15229 } else if (nodeName == 'li' && bookmark) { 15230 // Start wrapping - if we are in a list node and have a bookmark, then we will always begin by wrapping in a new element. 15231 currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process); 15232 } else { 15233 // Start a new wrapper for possible children 15234 currentWrapElm = 0; 15235 15236 each(tinymce.grep(node.childNodes), process); 15237 15238 if (hasContentEditableState) { 15239 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 15240 } 15241 15242 // End the last wrapper 15243 currentWrapElm = 0; 15244 } 15245 }; 15246 15247 // Process siblings from range 15248 each(nodes, process); 15249 }); 15250 15251 // Wrap links inside as well, for example color inside a link when the wrapper is around the link 15252 if (format.wrap_links === false) { 15253 each(newWrappers, function(node) { 15254 function process(node) { 15255 var i, currentWrapElm, children; 15256 15257 if (node.nodeName === 'A') { 15258 currentWrapElm = dom.clone(wrapElm, FALSE); 15259 newWrappers.push(currentWrapElm); 15260 15261 children = tinymce.grep(node.childNodes); 15262 for (i = 0; i < children.length; i++) 15263 currentWrapElm.appendChild(children[i]); 15264 15265 node.appendChild(currentWrapElm); 15266 } 15267 15268 each(tinymce.grep(node.childNodes), process); 15269 }; 15270 15271 process(node); 15272 }); 15273 } 15274 15275 // Cleanup 15276 15277 each(newWrappers, function(node) { 15278 var childCount; 15279 15280 function getChildCount(node) { 15281 var count = 0; 15282 15283 each(node.childNodes, function(node) { 15284 if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) 15285 count++; 15286 }); 15287 15288 return count; 15289 }; 15290 15291 function mergeStyles(node) { 15292 var child, clone; 15293 15294 each(node.childNodes, function(node) { 15295 if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { 15296 child = node; 15297 return FALSE; // break loop 15298 } 15299 }); 15300 15301 // If child was found and of the same type as the current node 15302 if (child && matchName(child, format)) { 15303 clone = dom.clone(child, FALSE); 15304 setElementFormat(clone); 15305 15306 dom.replace(clone, node, TRUE); 15307 dom.remove(child, 1); 15308 } 15309 15310 return clone || node; 15311 }; 15312 15313 childCount = getChildCount(node); 15314 15315 // Remove empty nodes but only if there is multiple wrappers and they are not block 15316 // elements so never remove single <h1></h1> since that would remove the currrent empty block element where the caret is at 15317 if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { 15318 dom.remove(node, 1); 15319 return; 15320 } 15321 15322 if (format.inline || format.wrapper) { 15323 // Merges the current node with it's children of similar type to reduce the number of elements 15324 if (!format.exact && childCount === 1) 15325 node = mergeStyles(node); 15326 15327 // Remove/merge children 15328 each(formatList, function(format) { 15329 // Merge all children of similar type will move styles from child to parent 15330 // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span> 15331 // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span> 15332 each(dom.select(format.inline, node), function(child) { 15333 var parent; 15334 15335 // When wrap_links is set to false we don't want 15336 // to remove the format on children within links 15337 if (format.wrap_links === false) { 15338 parent = child.parentNode; 15339 15340 do { 15341 if (parent.nodeName === 'A') 15342 return; 15343 } while (parent = parent.parentNode); 15344 } 15345 15346 removeFormat(format, vars, child, format.exact ? child : null); 15347 }); 15348 }); 15349 15350 // Remove child if direct parent is of same type 15351 if (matchNode(node.parentNode, name, vars)) { 15352 dom.remove(node, 1); 15353 node = 0; 15354 return TRUE; 15355 } 15356 15357 // Look for parent with similar style format 15358 if (format.merge_with_parents) { 15359 dom.getParent(node.parentNode, function(parent) { 15360 if (matchNode(parent, name, vars)) { 15361 dom.remove(node, 1); 15362 node = 0; 15363 return TRUE; 15364 } 15365 }); 15366 } 15367 15368 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b> 15369 if (node && format.merge_siblings !== false) { 15370 node = mergeSiblings(getNonWhiteSpaceSibling(node), node); 15371 node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); 15372 } 15373 } 15374 }); 15375 }; 15376 15377 if (format) { 15378 if (node) { 15379 if (node.nodeType) { 15380 rng = dom.createRng(); 15381 rng.setStartBefore(node); 15382 rng.setEndAfter(node); 15383 applyRngStyle(expandRng(rng, formatList), null, true); 15384 } else { 15385 applyRngStyle(node, null, true); 15386 } 15387 } else { 15388 if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { 15389 // Obtain selection node before selection is unselected by applyRngStyle() 15390 var curSelNode = ed.selection.getNode(); 15391 15392 // If the formats have a default block and we can't find a parent block then start wrapping it with a DIV this is for forced_root_blocks: false 15393 // It's kind of a hack but people should be using the default block type P since all desktop editors work that way 15394 if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { 15395 apply(formatList[0].defaultBlock); 15396 } 15397 15398 // Apply formatting to selection 15399 ed.selection.setRng(adjustSelectionToVisibleSelection()); 15400 bookmark = selection.getBookmark(); 15401 applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); 15402 15403 // Colored nodes should be underlined so that the color of the underline matches the text color. 15404 if (format.styles && (format.styles.color || format.styles.textDecoration)) { 15405 tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes'); 15406 processUnderlineAndColor(curSelNode); 15407 } 15408 15409 selection.moveToBookmark(bookmark); 15410 moveStart(selection.getRng(TRUE)); 15411 ed.nodeChanged(); 15412 } else 15413 performCaretAction('apply', name, vars); 15414 } 15415 } 15416 }; 15417 15418 function remove(name, vars, node) { 15419 var formatList = get(name), format = formatList[0], bookmark, i, rng, contentEditable = true; 15420 15421 // Merges the styles for each node 15422 function process(node) { 15423 var children, i, l, localContentEditable, lastContentEditable, hasContentEditableState; 15424 15425 // Node has a contentEditable value 15426 if (node.nodeType === 1 && getContentEditable(node)) { 15427 lastContentEditable = contentEditable; 15428 contentEditable = getContentEditable(node) === "true"; 15429 hasContentEditableState = true; // We don't want to wrap the container only it's children 15430 } 15431 15432 // Grab the children first since the nodelist might be changed 15433 children = tinymce.grep(node.childNodes); 15434 15435 // Process current node 15436 if (contentEditable && !hasContentEditableState) { 15437 for (i = 0, l = formatList.length; i < l; i++) { 15438 if (removeFormat(formatList[i], vars, node, node)) 15439 break; 15440 } 15441 } 15442 15443 // Process the children 15444 if (format.deep) { 15445 if (children.length) { 15446 for (i = 0, l = children.length; i < l; i++) 15447 process(children[i]); 15448 15449 if (hasContentEditableState) { 15450 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 15451 } 15452 } 15453 } 15454 }; 15455 15456 function findFormatRoot(container) { 15457 var formatRoot; 15458 15459 // Find format root 15460 each(getParents(container.parentNode).reverse(), function(parent) { 15461 var format; 15462 15463 // Find format root element 15464 if (!formatRoot && parent.id != '_start' && parent.id != '_end') { 15465 // Is the node matching the format we are looking for 15466 format = matchNode(parent, name, vars); 15467 if (format && format.split !== false) 15468 formatRoot = parent; 15469 } 15470 }); 15471 15472 return formatRoot; 15473 }; 15474 15475 function wrapAndSplit(format_root, container, target, split) { 15476 var parent, clone, lastClone, firstClone, i, formatRootParent; 15477 15478 // Format root found then clone formats and split it 15479 if (format_root) { 15480 formatRootParent = format_root.parentNode; 15481 15482 for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { 15483 clone = dom.clone(parent, FALSE); 15484 15485 for (i = 0; i < formatList.length; i++) { 15486 if (removeFormat(formatList[i], vars, clone, clone)) { 15487 clone = 0; 15488 break; 15489 } 15490 } 15491 15492 // Build wrapper node 15493 if (clone) { 15494 if (lastClone) 15495 clone.appendChild(lastClone); 15496 15497 if (!firstClone) 15498 firstClone = clone; 15499 15500 lastClone = clone; 15501 } 15502 } 15503 15504 // Never split block elements if the format is mixed 15505 if (split && (!format.mixed || !isBlock(format_root))) 15506 container = dom.split(format_root, container); 15507 15508 // Wrap container in cloned formats 15509 if (lastClone) { 15510 target.parentNode.insertBefore(lastClone, target); 15511 firstClone.appendChild(target); 15512 } 15513 } 15514 15515 return container; 15516 }; 15517 15518 function splitToFormatRoot(container) { 15519 return wrapAndSplit(findFormatRoot(container), container, container, true); 15520 }; 15521 15522 function unwrap(start) { 15523 var node = dom.get(start ? '_start' : '_end'), 15524 out = node[start ? 'firstChild' : 'lastChild']; 15525 15526 // If the end is placed within the start the result will be removed 15527 // So this checks if the out node is a bookmark node if it is it 15528 // checks for another more suitable node 15529 if (isBookmarkNode(out)) 15530 out = out[start ? 'firstChild' : 'lastChild']; 15531 15532 dom.remove(node, true); 15533 15534 return out; 15535 }; 15536 15537 function removeRngStyle(rng) { 15538 var startContainer, endContainer, node; 15539 15540 rng = expandRng(rng, formatList, TRUE); 15541 15542 if (format.split) { 15543 startContainer = getContainer(rng, TRUE); 15544 endContainer = getContainer(rng); 15545 15546 if (startContainer != endContainer) { 15547 // WebKit will render the table incorrectly if we wrap a TD in a SPAN so lets see if the can use the first child instead 15548 // This will happen if you tripple click a table cell and use remove formatting 15549 if (/^(TR|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { 15550 startContainer = (startContainer.nodeName == "TD" ? startContainer.firstChild : startContainer.firstChild.firstChild) || startContainer; 15551 } 15552 15553 // Wrap start/end nodes in span element since these might be cloned/moved 15554 startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'}); 15555 endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'}); 15556 15557 // Split start/end 15558 splitToFormatRoot(startContainer); 15559 splitToFormatRoot(endContainer); 15560 15561 // Unwrap start/end to get real elements again 15562 startContainer = unwrap(TRUE); 15563 endContainer = unwrap(); 15564 } else 15565 startContainer = endContainer = splitToFormatRoot(startContainer); 15566 15567 // Update range positions since they might have changed after the split operations 15568 rng.startContainer = startContainer.parentNode; 15569 rng.startOffset = nodeIndex(startContainer); 15570 rng.endContainer = endContainer.parentNode; 15571 rng.endOffset = nodeIndex(endContainer) + 1; 15572 } 15573 15574 // Remove items between start/end 15575 rangeUtils.walk(rng, function(nodes) { 15576 each(nodes, function(node) { 15577 process(node); 15578 15579 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. 15580 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') { 15581 removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node); 15582 } 15583 }); 15584 }); 15585 }; 15586 15587 // Handle node 15588 if (node) { 15589 if (node.nodeType) { 15590 rng = dom.createRng(); 15591 rng.setStartBefore(node); 15592 rng.setEndAfter(node); 15593 removeRngStyle(rng); 15594 } else { 15595 removeRngStyle(node); 15596 } 15597 15598 return; 15599 } 15600 15601 if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { 15602 bookmark = selection.getBookmark(); 15603 removeRngStyle(selection.getRng(TRUE)); 15604 selection.moveToBookmark(bookmark); 15605 15606 // Check if start element still has formatting then we are at: "<b>text|</b>text" and need to move the start into the next text node 15607 if (format.inline && match(name, vars, selection.getStart())) { 15608 moveStart(selection.getRng(true)); 15609 } 15610 15611 ed.nodeChanged(); 15612 } else 15613 performCaretAction('remove', name, vars); 15614 }; 15615 15616 function toggle(name, vars, node) { 15617 var fmt = get(name); 15618 15619 if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) 15620 remove(name, vars, node); 15621 else 15622 apply(name, vars, node); 15623 }; 15624 15625 function matchNode(node, name, vars, similar) { 15626 var formatList = get(name), format, i, classes; 15627 15628 function matchItems(node, format, item_name) { 15629 var key, value, items = format[item_name], i; 15630 15631 // Custom match 15632 if (format.onmatch) { 15633 return format.onmatch(node, format, item_name); 15634 } 15635 15636 // Check all items 15637 if (items) { 15638 // Non indexed object 15639 if (items.length === undef) { 15640 for (key in items) { 15641 if (items.hasOwnProperty(key)) { 15642 if (item_name === 'attributes') 15643 value = dom.getAttrib(node, key); 15644 else 15645 value = getStyle(node, key); 15646 15647 if (similar && !value && !format.exact) 15648 return; 15649 15650 if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars))) 15651 return; 15652 } 15653 } 15654 } else { 15655 // Only one match needed for indexed arrays 15656 for (i = 0; i < items.length; i++) { 15657 if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) 15658 return format; 15659 } 15660 } 15661 } 15662 15663 return format; 15664 }; 15665 15666 if (formatList && node) { 15667 // Check each format in list 15668 for (i = 0; i < formatList.length; i++) { 15669 format = formatList[i]; 15670 15671 // Name name, attributes, styles and classes 15672 if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { 15673 // Match classes 15674 if (classes = format.classes) { 15675 for (i = 0; i < classes.length; i++) { 15676 if (!dom.hasClass(node, classes[i])) 15677 return; 15678 } 15679 } 15680 15681 return format; 15682 } 15683 } 15684 } 15685 }; 15686 15687 function match(name, vars, node) { 15688 var startNode; 15689 15690 function matchParents(node) { 15691 // Find first node with similar format settings 15692 node = dom.getParent(node, function(node) { 15693 return !!matchNode(node, name, vars, true); 15694 }); 15695 15696 // Do an exact check on the similar format element 15697 return matchNode(node, name, vars); 15698 }; 15699 15700 // Check specified node 15701 if (node) 15702 return matchParents(node); 15703 15704 // Check selected node 15705 node = selection.getNode(); 15706 if (matchParents(node)) 15707 return TRUE; 15708 15709 // Check start node if it's different 15710 startNode = selection.getStart(); 15711 if (startNode != node) { 15712 if (matchParents(startNode)) 15713 return TRUE; 15714 } 15715 15716 return FALSE; 15717 }; 15718 15719 function matchAll(names, vars) { 15720 var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name; 15721 15722 // Check start of selection for formats 15723 startElement = selection.getStart(); 15724 dom.getParent(startElement, function(node) { 15725 var i, name; 15726 15727 for (i = 0; i < names.length; i++) { 15728 name = names[i]; 15729 15730 if (!checkedMap[name] && matchNode(node, name, vars)) { 15731 checkedMap[name] = true; 15732 matchedFormatNames.push(name); 15733 } 15734 } 15735 }, dom.getRoot()); 15736 15737 return matchedFormatNames; 15738 }; 15739 15740 function canApply(name) { 15741 var formatList = get(name), startNode, parents, i, x, selector; 15742 15743 if (formatList) { 15744 startNode = selection.getStart(); 15745 parents = getParents(startNode); 15746 15747 for (x = formatList.length - 1; x >= 0; x--) { 15748 selector = formatList[x].selector; 15749 15750 // Format is not selector based, then always return TRUE 15751 if (!selector) 15752 return TRUE; 15753 15754 for (i = parents.length - 1; i >= 0; i--) { 15755 if (dom.is(parents[i], selector)) 15756 return TRUE; 15757 } 15758 } 15759 } 15760 15761 return FALSE; 15762 }; 15763 15764 function formatChanged(formats, callback) { 15765 var currentFormats; 15766 15767 // Setup format node change logic 15768 if (!formatChangeData) { 15769 formatChangeData = {}; 15770 currentFormats = {}; 15771 15772 ed.onNodeChange.addToTop(function(ed, cm, node) { 15773 var parents = getParents(node), matchedFormats = {}; 15774 15775 // Check for new formats 15776 each(formatChangeData, function(callbacks, format) { 15777 each(parents, function(node) { 15778 if (matchNode(node, format, {}, true)) { 15779 if (!currentFormats[format]) { 15780 // Execute callbacks 15781 each(callbacks, function(callback) { 15782 callback(true, {node: node, format: format, parents: parents}); 15783 }); 15784 15785 currentFormats[format] = callbacks; 15786 } 15787 15788 matchedFormats[format] = callbacks; 15789 return false; 15790 } 15791 }); 15792 }); 15793 15794 // Check if current formats still match 15795 each(currentFormats, function(callbacks, format) { 15796 if (!matchedFormats[format]) { 15797 delete currentFormats[format]; 15798 15799 each(callbacks, function(callback) { 15800 callback(false, {node: node, format: format, parents: parents}); 15801 }); 15802 } 15803 }); 15804 }); 15805 } 15806 15807 // Add format listeners 15808 each(formats.split(','), function(format) { 15809 if (!formatChangeData[format]) { 15810 formatChangeData[format] = []; 15811 } 15812 15813 formatChangeData[format].push(callback); 15814 }); 15815 15816 return this; 15817 }; 15818 15819 // Expose to public 15820 tinymce.extend(this, { 15821 get : get, 15822 register : register, 15823 apply : apply, 15824 remove : remove, 15825 toggle : toggle, 15826 match : match, 15827 matchAll : matchAll, 15828 matchNode : matchNode, 15829 canApply : canApply, 15830 formatChanged: formatChanged 15831 }); 15832 15833 // Initialize 15834 defaultFormats(); 15835 addKeyboardShortcuts(); 15836 15837 // Private functions 15838 15839 function matchName(node, format) { 15840 // Check for inline match 15841 if (isEq(node, format.inline)) 15842 return TRUE; 15843 15844 // Check for block match 15845 if (isEq(node, format.block)) 15846 return TRUE; 15847 15848 // Check for selector match 15849 if (format.selector) 15850 return dom.is(node, format.selector); 15851 }; 15852 15853 function isEq(str1, str2) { 15854 str1 = str1 || ''; 15855 str2 = str2 || ''; 15856 15857 str1 = '' + (str1.nodeName || str1); 15858 str2 = '' + (str2.nodeName || str2); 15859 15860 return str1.toLowerCase() == str2.toLowerCase(); 15861 }; 15862 15863 function getStyle(node, name) { 15864 var styleVal = dom.getStyle(node, name); 15865 15866 // Force the format to hex 15867 if (name == 'color' || name == 'backgroundColor') 15868 styleVal = dom.toHex(styleVal); 15869 15870 // Opera will return bold as 700 15871 if (name == 'fontWeight' && styleVal == 700) 15872 styleVal = 'bold'; 15873 15874 return '' + styleVal; 15875 }; 15876 15877 function replaceVars(value, vars) { 15878 if (typeof(value) != "string") 15879 value = value(vars); 15880 else if (vars) { 15881 value = value.replace(/%(\w+)/g, function(str, name) { 15882 return vars[name] || str; 15883 }); 15884 } 15885 15886 return value; 15887 }; 15888 15889 function isWhiteSpaceNode(node) { 15890 return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); 15891 }; 15892 15893 function wrap(node, name, attrs) { 15894 var wrapper = dom.create(name, attrs); 15895 15896 node.parentNode.insertBefore(wrapper, node); 15897 wrapper.appendChild(node); 15898 15899 return wrapper; 15900 }; 15901 15902 function expandRng(rng, format, remove) { 15903 var sibling, lastIdx, leaf, endPoint, 15904 startContainer = rng.startContainer, 15905 startOffset = rng.startOffset, 15906 endContainer = rng.endContainer, 15907 endOffset = rng.endOffset; 15908 15909 // This function walks up the tree if there is no siblings before/after the node 15910 function findParentContainer(start) { 15911 var container, parent, child, sibling, siblingName, root; 15912 15913 container = parent = start ? startContainer : endContainer; 15914 siblingName = start ? 'previousSibling' : 'nextSibling'; 15915 root = dom.getRoot(); 15916 15917 // If it's a text node and the offset is inside the text 15918 if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { 15919 if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { 15920 return container; 15921 } 15922 } 15923 15924 for (;;) { 15925 // Stop expanding on block elements 15926 if (!format[0].block_expand && isBlock(parent)) 15927 return parent; 15928 15929 // Walk left/right 15930 for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { 15931 if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) { 15932 return parent; 15933 } 15934 } 15935 15936 // Check if we can move up are we at root level or body level 15937 if (parent.parentNode == root) { 15938 container = parent; 15939 break; 15940 } 15941 15942 parent = parent.parentNode; 15943 } 15944 15945 return container; 15946 }; 15947 15948 // This function walks down the tree to find the leaf at the selection. 15949 // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. 15950 function findLeaf(node, offset) { 15951 if (offset === undef) 15952 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 15953 while (node && node.hasChildNodes()) { 15954 node = node.childNodes[offset]; 15955 if (node) 15956 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 15957 } 15958 return { node: node, offset: offset }; 15959 } 15960 15961 // If index based start position then resolve it 15962 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { 15963 lastIdx = startContainer.childNodes.length - 1; 15964 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; 15965 15966 if (startContainer.nodeType == 3) 15967 startOffset = 0; 15968 } 15969 15970 // If index based end position then resolve it 15971 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { 15972 lastIdx = endContainer.childNodes.length - 1; 15973 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; 15974 15975 if (endContainer.nodeType == 3) 15976 endOffset = endContainer.nodeValue.length; 15977 } 15978 15979 // Expands the node to the closes contentEditable false element if it exists 15980 function findParentContentEditable(node) { 15981 var parent = node; 15982 15983 while (parent) { 15984 if (parent.nodeType === 1 && getContentEditable(parent)) { 15985 return getContentEditable(parent) === "false" ? parent : node; 15986 } 15987 15988 parent = parent.parentNode; 15989 } 15990 15991 return node; 15992 }; 15993 15994 function findWordEndPoint(container, offset, start) { 15995 var walker, node, pos, lastTextNode; 15996 15997 function findSpace(node, offset) { 15998 var pos, pos2, str = node.nodeValue; 15999 16000 if (typeof(offset) == "undefined") { 16001 offset = start ? str.length : 0; 16002 } 16003 16004 if (start) { 16005 pos = str.lastIndexOf(' ', offset); 16006 pos2 = str.lastIndexOf('\u00a0', offset); 16007 pos = pos > pos2 ? pos : pos2; 16008 16009 // Include the space on remove to avoid tag soup 16010 if (pos !== -1 && !remove) { 16011 pos++; 16012 } 16013 } else { 16014 pos = str.indexOf(' ', offset); 16015 pos2 = str.indexOf('\u00a0', offset); 16016 pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; 16017 } 16018 16019 return pos; 16020 }; 16021 16022 if (container.nodeType === 3) { 16023 pos = findSpace(container, offset); 16024 16025 if (pos !== -1) { 16026 return {container : container, offset : pos}; 16027 } 16028 16029 lastTextNode = container; 16030 } 16031 16032 // Walk the nodes inside the block 16033 walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); 16034 while (node = walker[start ? 'prev' : 'next']()) { 16035 if (node.nodeType === 3) { 16036 lastTextNode = node; 16037 pos = findSpace(node); 16038 16039 if (pos !== -1) { 16040 return {container : node, offset : pos}; 16041 } 16042 } else if (isBlock(node)) { 16043 break; 16044 } 16045 } 16046 16047 if (lastTextNode) { 16048 if (start) { 16049 offset = 0; 16050 } else { 16051 offset = lastTextNode.length; 16052 } 16053 16054 return {container: lastTextNode, offset: offset}; 16055 } 16056 }; 16057 16058 function findSelectorEndPoint(container, sibling_name) { 16059 var parents, i, y, curFormat; 16060 16061 if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) 16062 container = container[sibling_name]; 16063 16064 parents = getParents(container); 16065 for (i = 0; i < parents.length; i++) { 16066 for (y = 0; y < format.length; y++) { 16067 curFormat = format[y]; 16068 16069 // If collapsed state is set then skip formats that doesn't match that 16070 if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) 16071 continue; 16072 16073 if (dom.is(parents[i], curFormat.selector)) 16074 return parents[i]; 16075 } 16076 } 16077 16078 return container; 16079 }; 16080 16081 function findBlockEndPoint(container, sibling_name, sibling_name2) { 16082 var node; 16083 16084 // Expand to block of similar type 16085 if (!format[0].wrapper) 16086 node = dom.getParent(container, format[0].block); 16087 16088 // Expand to first wrappable block element or any block element 16089 if (!node) 16090 node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock); 16091 16092 // Exclude inner lists from wrapping 16093 if (node && format[0].wrapper) 16094 node = getParents(node, 'ul,ol').reverse()[0] || node; 16095 16096 // Didn't find a block element look for first/last wrappable element 16097 if (!node) { 16098 node = container; 16099 16100 while (node[sibling_name] && !isBlock(node[sibling_name])) { 16101 node = node[sibling_name]; 16102 16103 // Break on BR but include it will be removed later on 16104 // we can't remove it now since we need to check if it can be wrapped 16105 if (isEq(node, 'br')) 16106 break; 16107 } 16108 } 16109 16110 return node || container; 16111 }; 16112 16113 // Expand to closest contentEditable element 16114 startContainer = findParentContentEditable(startContainer); 16115 endContainer = findParentContentEditable(endContainer); 16116 16117 // Exclude bookmark nodes if possible 16118 if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { 16119 startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; 16120 startContainer = startContainer.nextSibling || startContainer; 16121 16122 if (startContainer.nodeType == 3) 16123 startOffset = 0; 16124 } 16125 16126 if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { 16127 endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; 16128 endContainer = endContainer.previousSibling || endContainer; 16129 16130 if (endContainer.nodeType == 3) 16131 endOffset = endContainer.length; 16132 } 16133 16134 if (format[0].inline) { 16135 if (rng.collapsed) { 16136 // Expand left to closest word boundery 16137 endPoint = findWordEndPoint(startContainer, startOffset, true); 16138 if (endPoint) { 16139 startContainer = endPoint.container; 16140 startOffset = endPoint.offset; 16141 } 16142 16143 // Expand right to closest word boundery 16144 endPoint = findWordEndPoint(endContainer, endOffset); 16145 if (endPoint) { 16146 endContainer = endPoint.container; 16147 endOffset = endPoint.offset; 16148 } 16149 } 16150 16151 // Avoid applying formatting to a trailing space. 16152 leaf = findLeaf(endContainer, endOffset); 16153 if (leaf.node) { 16154 while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) 16155 leaf = findLeaf(leaf.node.previousSibling); 16156 16157 if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && 16158 leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { 16159 16160 if (leaf.offset > 1) { 16161 endContainer = leaf.node; 16162 endContainer.splitText(leaf.offset - 1); 16163 } 16164 } 16165 } 16166 } 16167 16168 // Move start/end point up the tree if the leaves are sharp and if we are in different containers 16169 // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>! 16170 // This will reduce the number of wrapper elements that needs to be created 16171 // Move start point up the tree 16172 if (format[0].inline || format[0].block_expand) { 16173 if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { 16174 startContainer = findParentContainer(true); 16175 } 16176 16177 if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { 16178 endContainer = findParentContainer(); 16179 } 16180 } 16181 16182 // Expand start/end container to matching selector 16183 if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { 16184 // Find new startContainer/endContainer if there is better one 16185 startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); 16186 endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); 16187 } 16188 16189 // Expand start/end container to matching block element or text node 16190 if (format[0].block || format[0].selector) { 16191 // Find new startContainer/endContainer if there is better one 16192 startContainer = findBlockEndPoint(startContainer, 'previousSibling'); 16193 endContainer = findBlockEndPoint(endContainer, 'nextSibling'); 16194 16195 // Non block element then try to expand up the leaf 16196 if (format[0].block) { 16197 if (!isBlock(startContainer)) 16198 startContainer = findParentContainer(true); 16199 16200 if (!isBlock(endContainer)) 16201 endContainer = findParentContainer(); 16202 } 16203 } 16204 16205 // Setup index for startContainer 16206 if (startContainer.nodeType == 1) { 16207 startOffset = nodeIndex(startContainer); 16208 startContainer = startContainer.parentNode; 16209 } 16210 16211 // Setup index for endContainer 16212 if (endContainer.nodeType == 1) { 16213 endOffset = nodeIndex(endContainer) + 1; 16214 endContainer = endContainer.parentNode; 16215 } 16216 16217 // Return new range like object 16218 return { 16219 startContainer : startContainer, 16220 startOffset : startOffset, 16221 endContainer : endContainer, 16222 endOffset : endOffset 16223 }; 16224 } 16225 16226 function removeFormat(format, vars, node, compare_node) { 16227 var i, attrs, stylesModified; 16228 16229 // Check if node matches format 16230 if (!matchName(node, format)) 16231 return FALSE; 16232 16233 // Should we compare with format attribs and styles 16234 if (format.remove != 'all') { 16235 // Remove styles 16236 each(format.styles, function(value, name) { 16237 value = replaceVars(value, vars); 16238 16239 // Indexed array 16240 if (typeof(name) === 'number') { 16241 name = value; 16242 compare_node = 0; 16243 } 16244 16245 if (!compare_node || isEq(getStyle(compare_node, name), value)) 16246 dom.setStyle(node, name, ''); 16247 16248 stylesModified = 1; 16249 }); 16250 16251 // Remove style attribute if it's empty 16252 if (stylesModified && dom.getAttrib(node, 'style') == '') { 16253 node.removeAttribute('style'); 16254 node.removeAttribute('data-mce-style'); 16255 } 16256 16257 // Remove attributes 16258 each(format.attributes, function(value, name) { 16259 var valueOut; 16260 16261 value = replaceVars(value, vars); 16262 16263 // Indexed array 16264 if (typeof(name) === 'number') { 16265 name = value; 16266 compare_node = 0; 16267 } 16268 16269 if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { 16270 // Keep internal classes 16271 if (name == 'class') { 16272 value = dom.getAttrib(node, name); 16273 if (value) { 16274 // Build new class value where everything is removed except the internal prefixed classes 16275 valueOut = ''; 16276 each(value.split(/\s+/), function(cls) { 16277 if (/mce\w+/.test(cls)) 16278 valueOut += (valueOut ? ' ' : '') + cls; 16279 }); 16280 16281 // We got some internal classes left 16282 if (valueOut) { 16283 dom.setAttrib(node, name, valueOut); 16284 return; 16285 } 16286 } 16287 } 16288 16289 // IE6 has a bug where the attribute doesn't get removed correctly 16290 if (name == "class") 16291 node.removeAttribute('className'); 16292 16293 // Remove mce prefixed attributes 16294 if (MCE_ATTR_RE.test(name)) 16295 node.removeAttribute('data-mce-' + name); 16296 16297 node.removeAttribute(name); 16298 } 16299 }); 16300 16301 // Remove classes 16302 each(format.classes, function(value) { 16303 value = replaceVars(value, vars); 16304 16305 if (!compare_node || dom.hasClass(compare_node, value)) 16306 dom.removeClass(node, value); 16307 }); 16308 16309 // Check for non internal attributes 16310 attrs = dom.getAttribs(node); 16311 for (i = 0; i < attrs.length; i++) { 16312 if (attrs[i].nodeName.indexOf('_') !== 0) 16313 return FALSE; 16314 } 16315 } 16316 16317 // Remove the inline child if it's empty for example <b> or <span> 16318 if (format.remove != 'none') { 16319 removeNode(node, format); 16320 return TRUE; 16321 } 16322 }; 16323 16324 function removeNode(node, format) { 16325 var parentNode = node.parentNode, rootBlockElm; 16326 16327 function find(node, next, inc) { 16328 node = getNonWhiteSpaceSibling(node, next, inc); 16329 16330 return !node || (node.nodeName == 'BR' || isBlock(node)); 16331 }; 16332 16333 if (format.block) { 16334 if (!forcedRootBlock) { 16335 // Append BR elements if needed before we remove the block 16336 if (isBlock(node) && !isBlock(parentNode)) { 16337 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) 16338 node.insertBefore(dom.create('br'), node.firstChild); 16339 16340 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) 16341 node.appendChild(dom.create('br')); 16342 } 16343 } else { 16344 // Wrap the block in a forcedRootBlock if we are at the root of document 16345 if (parentNode == dom.getRoot()) { 16346 if (!format.list_block || !isEq(node, format.list_block)) { 16347 each(tinymce.grep(node.childNodes), function(node) { 16348 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) { 16349 if (!rootBlockElm) 16350 rootBlockElm = wrap(node, forcedRootBlock); 16351 else 16352 rootBlockElm.appendChild(node); 16353 } else 16354 rootBlockElm = 0; 16355 }); 16356 } 16357 } 16358 } 16359 } 16360 16361 // Never remove nodes that isn't the specified inline element if a selector is specified too 16362 if (format.selector && format.inline && !isEq(format.inline, node)) 16363 return; 16364 16365 dom.remove(node, 1); 16366 }; 16367 16368 function getNonWhiteSpaceSibling(node, next, inc) { 16369 if (node) { 16370 next = next ? 'nextSibling' : 'previousSibling'; 16371 16372 for (node = inc ? node : node[next]; node; node = node[next]) { 16373 if (node.nodeType == 1 || !isWhiteSpaceNode(node)) 16374 return node; 16375 } 16376 } 16377 }; 16378 16379 function isBookmarkNode(node) { 16380 return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark'; 16381 }; 16382 16383 function mergeSiblings(prev, next) { 16384 var marker, sibling, tmpSibling; 16385 16386 function compareElements(node1, node2) { 16387 // Not the same name 16388 if (node1.nodeName != node2.nodeName) 16389 return FALSE; 16390 16391 function getAttribs(node) { 16392 var attribs = {}; 16393 16394 each(dom.getAttribs(node), function(attr) { 16395 var name = attr.nodeName.toLowerCase(); 16396 16397 // Don't compare internal attributes or style 16398 if (name.indexOf('_') !== 0 && name !== 'style') 16399 attribs[name] = dom.getAttrib(node, name); 16400 }); 16401 16402 return attribs; 16403 }; 16404 16405 function compareObjects(obj1, obj2) { 16406 var value, name; 16407 16408 for (name in obj1) { 16409 // Obj1 has item obj2 doesn't have 16410 if (obj1.hasOwnProperty(name)) { 16411 value = obj2[name]; 16412 16413 // Obj2 doesn't have obj1 item 16414 if (value === undef) 16415 return FALSE; 16416 16417 // Obj2 item has a different value 16418 if (obj1[name] != value) 16419 return FALSE; 16420 16421 // Delete similar value 16422 delete obj2[name]; 16423 } 16424 } 16425 16426 // Check if obj 2 has something obj 1 doesn't have 16427 for (name in obj2) { 16428 // Obj2 has item obj1 doesn't have 16429 if (obj2.hasOwnProperty(name)) 16430 return FALSE; 16431 } 16432 16433 return TRUE; 16434 }; 16435 16436 // Attribs are not the same 16437 if (!compareObjects(getAttribs(node1), getAttribs(node2))) 16438 return FALSE; 16439 16440 // Styles are not the same 16441 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) 16442 return FALSE; 16443 16444 return TRUE; 16445 }; 16446 16447 function findElementSibling(node, sibling_name) { 16448 for (sibling = node; sibling; sibling = sibling[sibling_name]) { 16449 if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) 16450 return node; 16451 16452 if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) 16453 return sibling; 16454 } 16455 16456 return node; 16457 }; 16458 16459 // Check if next/prev exists and that they are elements 16460 if (prev && next) { 16461 // If previous sibling is empty then jump over it 16462 prev = findElementSibling(prev, 'previousSibling'); 16463 next = findElementSibling(next, 'nextSibling'); 16464 16465 // Compare next and previous nodes 16466 if (compareElements(prev, next)) { 16467 // Append nodes between 16468 for (sibling = prev.nextSibling; sibling && sibling != next;) { 16469 tmpSibling = sibling; 16470 sibling = sibling.nextSibling; 16471 prev.appendChild(tmpSibling); 16472 } 16473 16474 // Remove next node 16475 dom.remove(next); 16476 16477 // Move children into prev node 16478 each(tinymce.grep(next.childNodes), function(node) { 16479 prev.appendChild(node); 16480 }); 16481 16482 return prev; 16483 } 16484 } 16485 16486 return next; 16487 }; 16488 16489 function isTextBlock(name) { 16490 return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name); 16491 }; 16492 16493 function getContainer(rng, start) { 16494 var container, offset, lastIdx, walker; 16495 16496 container = rng[start ? 'startContainer' : 'endContainer']; 16497 offset = rng[start ? 'startOffset' : 'endOffset']; 16498 16499 if (container.nodeType == 1) { 16500 lastIdx = container.childNodes.length - 1; 16501 16502 if (!start && offset) 16503 offset--; 16504 16505 container = container.childNodes[offset > lastIdx ? lastIdx : offset]; 16506 } 16507 16508 // If start text node is excluded then walk to the next node 16509 if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { 16510 container = new TreeWalker(container, ed.getBody()).next() || container; 16511 } 16512 16513 // If end text node is excluded then walk to the previous node 16514 if (container.nodeType === 3 && !start && offset === 0) { 16515 container = new TreeWalker(container, ed.getBody()).prev() || container; 16516 } 16517 16518 return container; 16519 }; 16520 16521 function performCaretAction(type, name, vars) { 16522 var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; 16523 16524 // Creates a caret container bogus element 16525 function createCaretContainer(fill) { 16526 var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); 16527 16528 if (fill) { 16529 caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR)); 16530 } 16531 16532 return caretContainer; 16533 }; 16534 16535 function isCaretContainerEmpty(node, nodes) { 16536 while (node) { 16537 if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) { 16538 return false; 16539 } 16540 16541 // Collect nodes 16542 if (nodes && node.nodeType === 1) { 16543 nodes.push(node); 16544 } 16545 16546 node = node.firstChild; 16547 } 16548 16549 return true; 16550 }; 16551 16552 // Returns any parent caret container element 16553 function getParentCaretContainer(node) { 16554 while (node) { 16555 if (node.id === caretContainerId) { 16556 return node; 16557 } 16558 16559 node = node.parentNode; 16560 } 16561 }; 16562 16563 // Finds the first text node in the specified node 16564 function findFirstTextNode(node) { 16565 var walker; 16566 16567 if (node) { 16568 walker = new TreeWalker(node, node); 16569 16570 for (node = walker.current(); node; node = walker.next()) { 16571 if (node.nodeType === 3) { 16572 return node; 16573 } 16574 } 16575 } 16576 }; 16577 16578 // Removes the caret container for the specified node or all on the current document 16579 function removeCaretContainer(node, move_caret) { 16580 var child, rng; 16581 16582 if (!node) { 16583 node = getParentCaretContainer(selection.getStart()); 16584 16585 if (!node) { 16586 while (node = dom.get(caretContainerId)) { 16587 removeCaretContainer(node, false); 16588 } 16589 } 16590 } else { 16591 rng = selection.getRng(true); 16592 16593 if (isCaretContainerEmpty(node)) { 16594 if (move_caret !== false) { 16595 rng.setStartBefore(node); 16596 rng.setEndBefore(node); 16597 } 16598 16599 dom.remove(node); 16600 } else { 16601 child = findFirstTextNode(node); 16602 16603 if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) { 16604 child = child.deleteData(0, 1); 16605 } 16606 16607 dom.remove(node, 1); 16608 } 16609 16610 selection.setRng(rng); 16611 } 16612 }; 16613 16614 // Applies formatting to the caret postion 16615 function applyCaretFormat() { 16616 var rng, caretContainer, textNode, offset, bookmark, container, text; 16617 16618 rng = selection.getRng(true); 16619 offset = rng.startOffset; 16620 container = rng.startContainer; 16621 text = container.nodeValue; 16622 16623 caretContainer = getParentCaretContainer(selection.getStart()); 16624 if (caretContainer) { 16625 textNode = findFirstTextNode(caretContainer); 16626 } 16627 16628 // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character 16629 if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { 16630 // Get bookmark of caret position 16631 bookmark = selection.getBookmark(); 16632 16633 // Collapse bookmark range (WebKit) 16634 rng.collapse(true); 16635 16636 // Expand the range to the closest word and split it at those points 16637 rng = expandRng(rng, get(name)); 16638 rng = rangeUtils.split(rng); 16639 16640 // Apply the format to the range 16641 apply(name, vars, rng); 16642 16643 // Move selection back to caret position 16644 selection.moveToBookmark(bookmark); 16645 } else { 16646 if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) { 16647 caretContainer = createCaretContainer(true); 16648 textNode = caretContainer.firstChild; 16649 16650 rng.insertNode(caretContainer); 16651 offset = 1; 16652 16653 apply(name, vars, caretContainer); 16654 } else { 16655 apply(name, vars, caretContainer); 16656 } 16657 16658 // Move selection to text node 16659 selection.setCursorLocation(textNode, offset); 16660 } 16661 }; 16662 16663 function removeCaretFormat() { 16664 var rng = selection.getRng(true), container, offset, bookmark, 16665 hasContentAfter, node, formatNode, parents = [], i, caretContainer; 16666 16667 container = rng.startContainer; 16668 offset = rng.startOffset; 16669 node = container; 16670 16671 if (container.nodeType == 3) { 16672 if (offset != container.nodeValue.length || container.nodeValue === INVISIBLE_CHAR) { 16673 hasContentAfter = true; 16674 } 16675 16676 node = node.parentNode; 16677 } 16678 16679 while (node) { 16680 if (matchNode(node, name, vars)) { 16681 formatNode = node; 16682 break; 16683 } 16684 16685 if (node.nextSibling) { 16686 hasContentAfter = true; 16687 } 16688 16689 parents.push(node); 16690 node = node.parentNode; 16691 } 16692 16693 // Node doesn't have the specified format 16694 if (!formatNode) { 16695 return; 16696 } 16697 16698 // Is there contents after the caret then remove the format on the element 16699 if (hasContentAfter) { 16700 // Get bookmark of caret position 16701 bookmark = selection.getBookmark(); 16702 16703 // Collapse bookmark range (WebKit) 16704 rng.collapse(true); 16705 16706 // Expand the range to the closest word and split it at those points 16707 rng = expandRng(rng, get(name), true); 16708 rng = rangeUtils.split(rng); 16709 16710 // Remove the format from the range 16711 remove(name, vars, rng); 16712 16713 // Move selection back to caret position 16714 selection.moveToBookmark(bookmark); 16715 } else { 16716 caretContainer = createCaretContainer(); 16717 16718 node = caretContainer; 16719 for (i = parents.length - 1; i >= 0; i--) { 16720 node.appendChild(dom.clone(parents[i], false)); 16721 node = node.firstChild; 16722 } 16723 16724 // Insert invisible character into inner most format element 16725 node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR)); 16726 node = node.firstChild; 16727 16728 // Insert caret container after the formated node 16729 dom.insertAfter(caretContainer, formatNode); 16730 16731 // Move selection to text node 16732 selection.setCursorLocation(node, 1); 16733 } 16734 }; 16735 16736 // Checks if the parent caret container node isn't empty if that is the case it 16737 // will remove the bogus state on all children that isn't empty 16738 function unmarkBogusCaretParents() { 16739 var i, caretContainer, node; 16740 16741 caretContainer = getParentCaretContainer(selection.getStart()); 16742 if (caretContainer && !dom.isEmpty(caretContainer)) { 16743 tinymce.walk(caretContainer, function(node) { 16744 if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { 16745 dom.setAttrib(node, 'data-mce-bogus', null); 16746 } 16747 }, 'childNodes'); 16748 } 16749 }; 16750 16751 // Only bind the caret events once 16752 if (!self._hasCaretEvents) { 16753 // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements 16754 ed.onBeforeGetContent.addToTop(function() { 16755 var nodes = [], i; 16756 16757 if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { 16758 // Mark children 16759 i = nodes.length; 16760 while (i--) { 16761 dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); 16762 } 16763 } 16764 }); 16765 16766 // Remove caret container on mouse up and on key up 16767 tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) { 16768 ed[name].addToTop(function() { 16769 removeCaretContainer(); 16770 unmarkBogusCaretParents(); 16771 }); 16772 }); 16773 16774 // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys 16775 ed.onKeyDown.addToTop(function(ed, e) { 16776 var keyCode = e.keyCode; 16777 16778 if (keyCode == 8 || keyCode == 37 || keyCode == 39) { 16779 removeCaretContainer(getParentCaretContainer(selection.getStart())); 16780 } 16781 16782 unmarkBogusCaretParents(); 16783 }); 16784 16785 // Remove bogus state if they got filled by contents using editor.selection.setContent 16786 selection.onSetContent.add(unmarkBogusCaretParents); 16787 16788 self._hasCaretEvents = true; 16789 } 16790 16791 // Do apply or remove caret format 16792 if (type == "apply") { 16793 applyCaretFormat(); 16794 } else { 16795 removeCaretFormat(); 16796 } 16797 }; 16798 16799 function moveStart(rng) { 16800 var container = rng.startContainer, 16801 offset = rng.startOffset, isAtEndOfText, 16802 walker, node, nodes, tmpNode; 16803 16804 // Convert text node into index if possible 16805 if (container.nodeType == 3 && offset >= container.nodeValue.length) { 16806 // Get the parent container location and walk from there 16807 offset = nodeIndex(container); 16808 container = container.parentNode; 16809 isAtEndOfText = true; 16810 } 16811 16812 // Move startContainer/startOffset in to a suitable node 16813 if (container.nodeType == 1) { 16814 nodes = container.childNodes; 16815 container = nodes[Math.min(offset, nodes.length - 1)]; 16816 walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); 16817 16818 // If offset is at end of the parent node walk to the next one 16819 if (offset > nodes.length - 1 || isAtEndOfText) 16820 walker.next(); 16821 16822 for (node = walker.current(); node; node = walker.next()) { 16823 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { 16824 // IE has a "neat" feature where it moves the start node into the closest element 16825 // we can avoid this by inserting an element before it and then remove it after we set the selection 16826 tmpNode = dom.create('a', null, INVISIBLE_CHAR); 16827 node.parentNode.insertBefore(tmpNode, node); 16828 16829 // Set selection and remove tmpNode 16830 rng.setStart(node, 0); 16831 selection.setRng(rng); 16832 dom.remove(tmpNode); 16833 16834 return; 16835 } 16836 } 16837 } 16838 }; 16839 }; 16840 })(tinymce); 16841 16842 tinymce.onAddEditor.add(function(tinymce, ed) { 16843 var filters, fontSizes, dom, settings = ed.settings; 16844 16845 function replaceWithSpan(node, styles) { 16846 tinymce.each(styles, function(value, name) { 16847 if (value) 16848 dom.setStyle(node, name, value); 16849 }); 16850 16851 dom.rename(node, 'span'); 16852 }; 16853 16854 function convert(editor, params) { 16855 dom = editor.dom; 16856 16857 if (settings.convert_fonts_to_spans) { 16858 tinymce.each(dom.select('font,u,strike', params.node), function(node) { 16859 filters[node.nodeName.toLowerCase()](ed.dom, node); 16860 }); 16861 } 16862 }; 16863 16864 if (settings.inline_styles) { 16865 fontSizes = tinymce.explode(settings.font_size_legacy_values); 16866 16867 filters = { 16868 font : function(dom, node) { 16869 replaceWithSpan(node, { 16870 backgroundColor : node.style.backgroundColor, 16871 color : node.color, 16872 fontFamily : node.face, 16873 fontSize : fontSizes[parseInt(node.size, 10) - 1] 16874 }); 16875 }, 16876 16877 u : function(dom, node) { 16878 replaceWithSpan(node, { 16879 textDecoration : 'underline' 16880 }); 16881 }, 16882 16883 strike : function(dom, node) { 16884 replaceWithSpan(node, { 16885 textDecoration : 'line-through' 16886 }); 16887 } 16888 }; 16889 16890 ed.onPreProcess.add(convert); 16891 ed.onSetContent.add(convert); 16892 16893 ed.onInit.add(function() { 16894 ed.selection.onSetContent.add(convert); 16895 }); 16896 } 16897 }); 16898 16899 (function(tinymce) { 16900 var TreeWalker = tinymce.dom.TreeWalker; 16901 16902 tinymce.EnterKey = function(editor) { 16903 var dom = editor.dom, selection = editor.selection, settings = editor.settings, undoManager = editor.undoManager, nonEmptyElementsMap = editor.schema.getNonEmptyElements(); 16904 16905 function handleEnterKey(evt) { 16906 var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, 16907 newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; 16908 16909 // Returns true if the block can be split into two blocks or not 16910 function canSplitBlock(node) { 16911 return node && 16912 dom.isBlock(node) && 16913 !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && 16914 !/^(fixed|absolute)/i.test(node.style.position) && 16915 dom.getContentEditable(node) !== "true"; 16916 }; 16917 16918 // Renders empty block on IE 16919 function renderBlockOnIE(block) { 16920 var oldRng; 16921 16922 if (tinymce.isIE && dom.isBlock(block)) { 16923 oldRng = selection.getRng(); 16924 block.appendChild(dom.create('span', null, '\u00a0')); 16925 selection.select(block); 16926 block.lastChild.outerHTML = ''; 16927 selection.setRng(oldRng); 16928 } 16929 }; 16930 16931 // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p> 16932 function trimInlineElementsOnLeftSideOfBlock(block) { 16933 var node = block, firstChilds = [], i; 16934 16935 // Find inner most first child ex: <p><i><b>*</b></i></p> 16936 while (node = node.firstChild) { 16937 if (dom.isBlock(node)) { 16938 return; 16939 } 16940 16941 if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 16942 firstChilds.push(node); 16943 } 16944 } 16945 16946 i = firstChilds.length; 16947 while (i--) { 16948 node = firstChilds[i]; 16949 if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { 16950 dom.remove(node); 16951 } 16952 } 16953 }; 16954 16955 // Moves the caret to a suitable position within the root for example in the first non pure whitespace text node or before an image 16956 function moveToCaretPosition(root) { 16957 var walker, node, rng, y, viewPort, lastNode = root, tempElm; 16958 16959 rng = dom.createRng(); 16960 16961 if (root.hasChildNodes()) { 16962 walker = new TreeWalker(root, root); 16963 16964 while (node = walker.current()) { 16965 if (node.nodeType == 3) { 16966 rng.setStart(node, 0); 16967 rng.setEnd(node, 0); 16968 break; 16969 } 16970 16971 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 16972 rng.setStartBefore(node); 16973 rng.setEndBefore(node); 16974 break; 16975 } 16976 16977 lastNode = node; 16978 node = walker.next(); 16979 } 16980 16981 if (!node) { 16982 rng.setStart(lastNode, 0); 16983 rng.setEnd(lastNode, 0); 16984 } 16985 } else { 16986 if (root.nodeName == 'BR') { 16987 if (root.nextSibling && dom.isBlock(root.nextSibling)) { 16988 // Trick on older IE versions to render the caret before the BR between two lists 16989 if (!documentMode || documentMode < 9) { 16990 tempElm = dom.create('br'); 16991 root.parentNode.insertBefore(tempElm, root); 16992 } 16993 16994 rng.setStartBefore(root); 16995 rng.setEndBefore(root); 16996 } else { 16997 rng.setStartAfter(root); 16998 rng.setEndAfter(root); 16999 } 17000 } else { 17001 rng.setStart(root, 0); 17002 rng.setEnd(root, 0); 17003 } 17004 } 17005 17006 selection.setRng(rng); 17007 17008 // Remove tempElm created for old IE:s 17009 dom.remove(tempElm); 17010 17011 viewPort = dom.getViewPort(editor.getWin()); 17012 17013 // scrollIntoView seems to scroll the parent window in most browsers now including FF 3.0b4 so it's time to stop using it and do it our selfs 17014 y = dom.getPos(root).y; 17015 if (y < viewPort.y || y + 25 > viewPort.y + viewPort.h) { 17016 editor.getWin().scrollTo(0, y < viewPort.y ? y : y - viewPort.h + 25); // Needs to be hardcoded to roughly one line of text if a huge text block is broken into two blocks 17017 } 17018 }; 17019 17020 // Creates a new block element by cloning the current one or creating a new one if the name is specified 17021 // This function will also copy any text formatting from the parent block and add it to the new one 17022 function createNewBlock(name) { 17023 var node = container, block, clonedNode, caretNode; 17024 17025 block = name || parentBlockName == "TABLE" ? dom.create(name || newBlockName) : parentBlock.cloneNode(false); 17026 caretNode = block; 17027 17028 // Clone any parent styles 17029 if (settings.keep_styles !== false) { 17030 do { 17031 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) { 17032 clonedNode = node.cloneNode(false); 17033 dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique 17034 17035 if (block.hasChildNodes()) { 17036 clonedNode.appendChild(block.firstChild); 17037 block.appendChild(clonedNode); 17038 } else { 17039 caretNode = clonedNode; 17040 block.appendChild(clonedNode); 17041 } 17042 } 17043 } while (node = node.parentNode); 17044 } 17045 17046 // BR is needed in empty blocks on non IE browsers 17047 if (!tinymce.isIE) { 17048 caretNode.innerHTML = '<br>'; 17049 } 17050 17051 return block; 17052 }; 17053 17054 // Returns true/false if the caret is at the start/end of the parent block element 17055 function isCaretAtStartOrEndOfBlock(start) { 17056 var walker, node, name; 17057 17058 // Caret is in the middle of a text node like "a|b" 17059 if (container.nodeType == 3 && (start ? offset > 0 : offset < container.nodeValue.length)) { 17060 return false; 17061 } 17062 17063 // If after the last element in block node edge case for #5091 17064 if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) { 17065 return true; 17066 } 17067 17068 // If the caret if before the first element in parentBlock 17069 if (start && container.nodeType == 1 && container == parentBlock.firstChild) { 17070 return true; 17071 } 17072 17073 // Caret can be before/after a table 17074 if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) { 17075 return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); 17076 } 17077 17078 // Walk the DOM and look for text nodes or non empty elements 17079 walker = new TreeWalker(container, parentBlock); 17080 17081 // If caret is in beginning or end of a text block then jump to the next/previous node 17082 if (container.nodeType == 3) { 17083 if (start && offset == 0) { 17084 walker.prev(); 17085 } else if (!start && offset == container.nodeValue.length) { 17086 walker.next(); 17087 } 17088 } 17089 17090 while (node = walker.current()) { 17091 if (node.nodeType === 1) { 17092 // Ignore bogus elements 17093 if (!node.getAttribute('data-mce-bogus')) { 17094 // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p> 17095 name = node.nodeName.toLowerCase(); 17096 if (nonEmptyElementsMap[name] && name !== 'br') { 17097 return false; 17098 } 17099 } 17100 } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) { 17101 return false; 17102 } 17103 17104 if (start) { 17105 walker.prev(); 17106 } else { 17107 walker.next(); 17108 } 17109 } 17110 17111 return true; 17112 }; 17113 17114 // Wraps any text nodes or inline elements in the specified forced root block name 17115 function wrapSelfAndSiblingsInDefaultBlock(container, offset) { 17116 var newBlock, parentBlock, startNode, node, next, blockName = newBlockName || 'P'; 17117 17118 // Not in a block element or in a table cell or caption 17119 parentBlock = dom.getParent(container, dom.isBlock); 17120 if (!parentBlock || !canSplitBlock(parentBlock)) { 17121 parentBlock = parentBlock || editableRoot; 17122 17123 if (!parentBlock.hasChildNodes()) { 17124 newBlock = dom.create(blockName); 17125 parentBlock.appendChild(newBlock); 17126 rng.setStart(newBlock, 0); 17127 rng.setEnd(newBlock, 0); 17128 return newBlock; 17129 } 17130 17131 // Find parent that is the first child of parentBlock 17132 node = container; 17133 while (node.parentNode != parentBlock) { 17134 node = node.parentNode; 17135 } 17136 17137 // Loop left to find start node start wrapping at 17138 while (node && !dom.isBlock(node)) { 17139 startNode = node; 17140 node = node.previousSibling; 17141 } 17142 17143 if (startNode) { 17144 newBlock = dom.create(blockName); 17145 startNode.parentNode.insertBefore(newBlock, startNode); 17146 17147 // Start wrapping until we hit a block 17148 node = startNode; 17149 while (node && !dom.isBlock(node)) { 17150 next = node.nextSibling; 17151 newBlock.appendChild(node); 17152 node = next; 17153 } 17154 17155 // Restore range to it's past location 17156 rng.setStart(container, offset); 17157 rng.setEnd(container, offset); 17158 } 17159 } 17160 17161 return container; 17162 }; 17163 17164 // Inserts a block or br before/after or in the middle of a split list of the LI is empty 17165 function handleEmptyListItem() { 17166 function isFirstOrLastLi(first) { 17167 var node = containerBlock[first ? 'firstChild' : 'lastChild']; 17168 17169 // Find first/last element since there might be whitespace there 17170 while (node) { 17171 if (node.nodeType == 1) { 17172 break; 17173 } 17174 17175 node = node[first ? 'nextSibling' : 'previousSibling']; 17176 } 17177 17178 return node === parentBlock; 17179 }; 17180 17181 newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR'); 17182 17183 if (isFirstOrLastLi(true) && isFirstOrLastLi()) { 17184 // Is first and last list item then replace the OL/UL with a text block 17185 dom.replace(newBlock, containerBlock); 17186 } else if (isFirstOrLastLi(true)) { 17187 // First LI in list then remove LI and add text block before list 17188 containerBlock.parentNode.insertBefore(newBlock, containerBlock); 17189 } else if (isFirstOrLastLi()) { 17190 // Last LI in list then temove LI and add text block after list 17191 dom.insertAfter(newBlock, containerBlock); 17192 renderBlockOnIE(newBlock); 17193 } else { 17194 // Middle LI in list the split the list and insert a text block in the middle 17195 // Extract after fragment and insert it after the current block 17196 tmpRng = rng.cloneRange(); 17197 tmpRng.setStartAfter(parentBlock); 17198 tmpRng.setEndAfter(containerBlock); 17199 fragment = tmpRng.extractContents(); 17200 dom.insertAfter(fragment, containerBlock); 17201 dom.insertAfter(newBlock, containerBlock); 17202 } 17203 17204 dom.remove(parentBlock); 17205 moveToCaretPosition(newBlock); 17206 undoManager.add(); 17207 }; 17208 17209 // Walks the parent block to the right and look for BR elements 17210 function hasRightSideBr() { 17211 var walker = new TreeWalker(container, parentBlock), node; 17212 17213 while (node = walker.current()) { 17214 if (node.nodeName == 'BR') { 17215 return true; 17216 } 17217 17218 node = walker.next(); 17219 } 17220 } 17221 17222 // Inserts a BR element if the forced_root_block option is set to false or empty string 17223 function insertBr() { 17224 var brElm, extraBr; 17225 17226 if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { 17227 // Insert extra BR element at the end block elements 17228 if (!tinymce.isIE && !hasRightSideBr()) { 17229 brElm = dom.create('br') 17230 rng.insertNode(brElm); 17231 rng.setStartAfter(brElm); 17232 rng.setEndAfter(brElm); 17233 extraBr = true; 17234 } 17235 } 17236 17237 brElm = dom.create('br'); 17238 rng.insertNode(brElm); 17239 17240 // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it 17241 if (tinymce.isIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { 17242 brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); 17243 } 17244 17245 if (!extraBr) { 17246 rng.setStartAfter(brElm); 17247 rng.setEndAfter(brElm); 17248 } else { 17249 rng.setStartBefore(brElm); 17250 rng.setEndBefore(brElm); 17251 } 17252 17253 selection.setRng(rng); 17254 undoManager.add(); 17255 }; 17256 17257 // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element 17258 function trimLeadingLineBreaks(node) { 17259 do { 17260 if (node.nodeType === 3) { 17261 node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, ''); 17262 } 17263 17264 node = node.firstChild; 17265 } while (node); 17266 }; 17267 17268 function getEditableRoot(node) { 17269 var root = dom.getRoot(), parent, editableRoot; 17270 17271 // Get all parents until we hit a non editable parent or the root 17272 parent = node; 17273 while (parent !== root && dom.getContentEditable(parent) !== "false") { 17274 if (dom.getContentEditable(parent) === "true") { 17275 editableRoot = parent; 17276 } 17277 17278 parent = parent.parentNode; 17279 } 17280 17281 return parent !== root ? editableRoot : root; 17282 }; 17283 17284 // Adds a BR at the end of blocks that only contains an IMG or INPUT since these might be floated and then they won't expand the block 17285 function addBrToBlockIfNeeded(block) { 17286 var lastChild; 17287 17288 // IE will render the blocks correctly other browsers needs a BR 17289 if (!tinymce.isIE) { 17290 block.normalize(); // Remove empty text nodes that got left behind by the extract 17291 17292 // Check if the block is empty or contains a floated last child 17293 lastChild = block.lastChild; 17294 if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { 17295 dom.add(block, 'br'); 17296 } 17297 } 17298 }; 17299 17300 // Delete any selected contents 17301 if (!rng.collapsed) { 17302 editor.execCommand('Delete'); 17303 return; 17304 } 17305 17306 // Event is blocked by some other handler for example the lists plugin 17307 if (evt.isDefaultPrevented()) { 17308 return; 17309 } 17310 17311 // Setup range items and newBlockName 17312 container = rng.startContainer; 17313 offset = rng.startOffset; 17314 newBlockName = settings.forced_root_block; 17315 newBlockName = newBlockName ? newBlockName.toUpperCase() : ''; 17316 documentMode = dom.doc.documentMode; 17317 17318 // Resolve node index 17319 if (container.nodeType == 1 && container.hasChildNodes()) { 17320 isAfterLastNodeInContainer = offset > container.childNodes.length - 1; 17321 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; 17322 if (isAfterLastNodeInContainer && container.nodeType == 3) { 17323 offset = container.nodeValue.length; 17324 } else { 17325 offset = 0; 17326 } 17327 } 17328 17329 // Get editable root node normaly the body element but sometimes a div or span 17330 editableRoot = getEditableRoot(container); 17331 17332 // If there is no editable root then enter is done inside a contentEditable false element 17333 if (!editableRoot) { 17334 return; 17335 } 17336 17337 undoManager.beforeChange(); 17338 17339 // If editable root isn't block nor the root of the editor 17340 if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) { 17341 if (!newBlockName || evt.shiftKey) { 17342 insertBr(); 17343 } 17344 17345 return; 17346 } 17347 17348 // Wrap the current node and it's sibling in a default block if it's needed. 17349 // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td> 17350 // This won't happen if root blocks are disabled or the shiftKey is pressed 17351 if ((newBlockName && !evt.shiftKey) || (!newBlockName && evt.shiftKey)) { 17352 container = wrapSelfAndSiblingsInDefaultBlock(container, offset); 17353 } 17354 17355 // Find parent block and setup empty block paddings 17356 parentBlock = dom.getParent(container, dom.isBlock); 17357 containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; 17358 17359 // Setup block names 17360 parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 17361 containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 17362 17363 // Handle enter inside an empty list item 17364 if (parentBlockName == 'LI' && dom.isEmpty(parentBlock)) { 17365 // Let the list plugin or browser handle nested lists for now 17366 if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) { 17367 return false; 17368 } 17369 17370 handleEmptyListItem(); 17371 return; 17372 } 17373 17374 // Don't split PRE tags but insert a BR instead easier when writing code samples etc 17375 if (parentBlockName == 'PRE' && settings.br_in_pre !== false) { 17376 if (!evt.shiftKey) { 17377 insertBr(); 17378 return; 17379 } 17380 } else { 17381 // If no root block is configured then insert a BR by default or if the shiftKey is pressed 17382 if ((!newBlockName && !evt.shiftKey && parentBlockName != 'LI') || (newBlockName && evt.shiftKey)) { 17383 insertBr(); 17384 return; 17385 } 17386 } 17387 17388 // Default block name if it's not configured 17389 newBlockName = newBlockName || 'P'; 17390 17391 // Insert new block before/after the parent block depending on caret location 17392 if (isCaretAtStartOrEndOfBlock()) { 17393 // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup 17394 if (/^(H[1-6]|PRE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') { 17395 newBlock = createNewBlock(newBlockName); 17396 } else { 17397 newBlock = createNewBlock(); 17398 } 17399 17400 // Split the current container block element if enter is pressed inside an empty inner block element 17401 if (settings.end_container_on_empty_block && canSplitBlock(containerBlock) && dom.isEmpty(parentBlock)) { 17402 // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P 17403 newBlock = dom.split(containerBlock, parentBlock); 17404 } else { 17405 dom.insertAfter(newBlock, parentBlock); 17406 } 17407 17408 moveToCaretPosition(newBlock); 17409 } else if (isCaretAtStartOrEndOfBlock(true)) { 17410 // Insert new block before 17411 newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); 17412 renderBlockOnIE(newBlock); 17413 } else { 17414 // Extract after fragment and insert it after the current block 17415 tmpRng = rng.cloneRange(); 17416 tmpRng.setEndAfter(parentBlock); 17417 fragment = tmpRng.extractContents(); 17418 trimLeadingLineBreaks(fragment); 17419 newBlock = fragment.firstChild; 17420 dom.insertAfter(fragment, parentBlock); 17421 trimInlineElementsOnLeftSideOfBlock(newBlock); 17422 addBrToBlockIfNeeded(parentBlock); 17423 moveToCaretPosition(newBlock); 17424 } 17425 17426 dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique 17427 undoManager.add(); 17428 } 17429 17430 editor.onKeyDown.add(function(ed, evt) { 17431 if (evt.keyCode == 13) { 17432 if (handleEnterKey(evt) !== false) { 17433 evt.preventDefault(); 17434 } 17435 } 17436 }); 17437 }; 17438 })(tinymce); 17439 17440